Skip to content
BuildThenTestedSee packages

Subscription state trusted from local storage

Anyone can type one line in the browser console to become a paying customer, and the server hands over the paid feature without checking. Everything between the pay button and the money arriving. Check it yourself with the steps below; our test adds what a script and a second account can find.

What it looks like

// after the Stripe success redirect
localStorage.setItem("plan", "pro");

// gating a paid feature
const isPro = localStorage.getItem("plan") === "pro";
if (isPro) showExportButton();
// the export API itself never checks the subscription
The browser stores the plan in localStorage (a small key-value store any script or user on that browser can edit) after checkout, and paid features switch on by reading it back.

What happens

Anyone can type one line in the browser console to become a paying customer, and the server hands over the paid feature without checking.

Where it shows up

Descriptive, not a verdict: builders and stacks whose defaults make this reachable, or that a public incident names. Each hub states the tool's own security features.

Seen in public

Two days after announcing a SaaS built with Cursor and no hand-written code, its founder reported subscription bypasses, maxed-out API keys and unwanted database writes; he shut the app down three days later.

  • 2025-03-15 Launch announcement
  • 2025-03-17 Report of subscription bypass and API key abuse
  • 2025-03-20 Shutdown announcement

Mar 2025 · Leo (Enrichlead founder) · all sources

Check it yourself

Uses your builder's dashboard and your browser. No tools to install.

  1. Sign in to your app with a free account, press F12 to open developer tools, select the Application tab, then Local Storage on the left.
  2. Look for keys such as plan, tier, isPro, subscription or premium. Double-click the value and change it to what a paid user would have, for example pro or true.
  3. Reload the page and use the paid feature, including any download, export or report it produces.
  4. Bad result: the feature works and the server returns real data. If the button appears but the server refuses, the gap is only cosmetic.

What we check

We call each paid feature's backend route directly with a free account's session, and again with client-side flags flipped, and report which routes serve paid data without a server-side subscription check.

Fix direction

Store the subscription status in your database, update it from verified Stripe webhook events, and have every paid route check that record before responding. The browser value can stay for showing the right buttons, never as the decision.

Mechanism documented in: Window: localStorage property (verified 2026-09-21); How Checkout works (verified 2026-09-21).

Covered by

Ready-to-charge check ($390, 48 hours) and Full audit ($1,900, 5 business days)

Both include a re-check after your fixes.

Compare packages

Related patterns

Questions

subscription status trusted from browser

Anything stored in the browser, including localStorage, cookies the app can read and in-memory state, belongs to the person using that browser and can be edited freely. A subscription flag there is fine for showing or hiding buttons. The decision to return paid data has to happen on the server, against a record written from Stripe's webhook.

subscription bypass client side check

The bypass is one edit in developer tools: change the stored plan value, reload, use the feature. If the paid route on the server trusts the browser, the data comes back. Try it with a free account on your own app; if it works for you, it works for everyone.

where should subscription status be stored

In your database, on the user or organisation record, set only by your webhook handler after it verifies the Stripe event. Stripe's Checkout flow is built around that webhook step for fulfilment. Every paid API route then reads that record before doing work.

Back to the catalogue, or run the self-check for your stack.