Subscription state trusted from local storage
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 subscriptionWhat 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.
- 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.
- 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.
- Reload the page and use the paid feature, including any download, export or report it produces.
- 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.
Related patterns
- Price or plan taken from the clientPaymentsHigh
A buyer can edit the request in their browser, pay one cent for the top plan, and your app records it as a full purchase.
- Authorization enforced only in the browserLogin and permissionsCritical
Anyone who calls the API directly, or edits the page in developer tools, gets the data and actions the screen was hiding.
- Stripe webhook accepted without signature verificationPaymentsCritical
Anyone who finds the webhook URL can post a made-up payment-succeeded event and get an order fulfilled or a plan activated for a user of their choosing, without paying.
- Tokens trusted without server verificationLogin and permissionsCritical
Anyone can write their own token naming another user, or an admin, and the server will act as that person.
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.