A client side paywall bypass is what happens when the only thing standing between a free user and paid content is code running inside that user’s browser. The server hands the full article or the full capability to everyone who asks, and the frontend decides whether to reveal it. Anyone who opens developer tools, edits a response, or reads the JavaScript bundle gets the paid thing for nothing. This post covers the four shapes it takes, why it is access control rather than styling, and why scanners walk past it.
What does a client side paywall bypass look like in practice?
It looks like a server that answers honestly and a browser that lies politely on its behalf. Take an invented product, Acme Ledger, a reporting SaaS with a Free and a Pro tier. A free reader opens a premium report.
GET /api/v1/reports/482
Authorization: Bearer <free tier session>
200 OK
{
"id": 482,
"title": "Q3 margin breakdown",
"preview": "The first two paragraphs...",
"body": "...the entire 2,400 word report, in full...",
"tier_required": "pro"
}
The body is already on the reader’s machine. The frontend then does the only enforcement in the system:
if (report.tier_required === "pro" && !user.isPro) {
return <PaywallOverlay preview={report.preview} />;
}
return <FullReport body={report.body} />;
Nothing here is broken the way a crash is broken. The page renders. The paywall appears. And a free user reads the whole report out of the network tab.
What are the four shapes this bug takes?
Four recurring shapes, one root: the server never checked entitlement, so the browser had to.
1. The hidden interface element
A button or a form is rendered with display: none or disabled, but the endpoint it points at is live and unguarded. Deleting an attribute in the inspector, or calling the endpoint, performs the action. The visual control was the whole control.
2. The client side role check
The bundle contains a line like if (user.role === "admin") or if (session.plan !== "free"). Both values arrive in a response the user can intercept, and the comparison runs on hardware the user owns. Editing "free" to "pro" flips every gate. Same failure class as broken function level authorization, moved from a forgotten server check to a check that was never on the server at all.
3. Content shipped, then masked
The paid text or the paid rows sit in the payload, and the frontend blurs, truncates, or removes them from the DOM after the page loads. Often a reader does not even need developer tools: disabling JavaScript leaves the content on screen. When the masked field is one attribute in an object otherwise fine to return, this shades into broken object property level authorization.
4. The API route the frontend simply does not call
The quietest one. Acme has /api/v1/reports/482/export for Pro accounts, and the Free interface never renders the export button, so no free session touched that route in testing. Nobody wrote a check on it, because in the only flow anyone looked at it was unreachable. A free session sends the request and gets the file.
The browser is not a place you can put a rule. It is a place you can put a hint, on a machine the attacker owns, for a program the attacker can rewrite.
Why is this an access control failure rather than a UI bug?
Because the damage is measured in data leaving the system, not in pixels. A UI bug means a user sees the wrong thing. Here a user obtains the thing: the paid report, the export, the allowance they did not buy. The interface was doing the job of an authorization layer, and it cannot, because it runs on the other side of the trust boundary.
It is also a business logic vulnerability. There is no injection and no malformed input. Every request is one the application meant to support. The flaw is in what it decided was allowed, a question about the product, not the syntax.
Why do AI generated applications produce this so often?
Because a code generator is asked for the visible behaviour, not the server rule. “Free users should not see the full report” describes a screen. “Free users must not receive the full report” describes an authorization decision. The first produces an overlay. The second produces a check in the handler. Prompts almost always take the first shape, and the result looks correct in the browser, which is where it gets reviewed.
Published third party research points the same direction. Scans of large numbers of vibe coded production applications have reported that a majority carried at least one security issue, and Imperva has published findings on authentication bypass in a named AI application builder. We have not tested those products, and no example here describes a real one. The pattern is the point: a paywall that looks right is the easiest kind to build with nothing behind it. Our overview of vibe coded app security covers the wider set of gaps, and Supabase row level security misconfiguration is the database shaped sibling of this mistake.
Why do scanners miss it?
Because there is nothing to match on. A scanner looks for inputs that make an application misbehave: a quote that breaks a query, a payload that echoes back. A paywall bypass has neither. The request is well formed, carries a real session, targets a documented endpoint, and the server answers it happily with a 200. On the wire, a free reader of a premium report looks identical to a paying one, because the server cannot tell them apart. Catching it means knowing what the product charges for, and intent never appears in a payload list.
How is this different from a normal authorization bug?
In a normal authorization bug the rule exists and fails on one path: a check present on nine endpoints and forgotten on the tenth. You find those by comparing paths, because a correct example sits next to the broken one.
In a client side bypass the rule was never written on the server at all. There is no correct path to compare against and no inconsistency to spot. The backend is consistent and completely open. That is why reading it leaves a reviewer feeling fine. Nothing looks wrong, because nothing is there.
How do you prevent it?
Enforce entitlement on the server for every request that returns paid data or performs a paid action, and treat the frontend as a rendering layer with zero authority.
- Never send data the user is not entitled to. If a free session cannot read the body, the body must not be in the response. Drop it at the query layer, not in the component.
- Check entitlement where the action happens. Not in the route that renders the page, not in a gateway three services ago. In the handler that reads or writes the record.
- Derive the plan from the server. Look up the account’s tier from your own store using the session identity. A plan field the client sent you is a wish, not a fact.
- Give every premium route its own check. Default to deny, so a new endpoint is closed until someone writes the rule rather than open until someone remembers.
- Test every premium route with a free account’s session. Skip the interface. Call each route from your API definition with a free tier token, and treat any 200 carrying paid data as a bug.
- Assume the bundle is public. Feature flags, route names, and role strings in shipped JavaScript are a map of what to try. Fine, as long as the map leads to closed doors.
What is the takeaway?
If you can describe your paid tier only by what the screen shows, you do not have a paid tier, you have a suggestion. The fix is not a better overlay. It is a server that refuses, on every request, to hand out something the account did not buy. Finding this takes someone who understands what an application is for before they can tell it is broken, which is what UnboundCompute is built to do. More on our about page.
Frequently asked questions
What is a client side paywall bypass?
It is an access control failure where the only thing enforcing a paid tier, a feature flag, or a role is code running in the user’s browser. The server returns the full content or the full capability to everyone, and the frontend decides whether to reveal it, so anyone who opens developer tools or calls the endpoint directly gets the paid thing for free.
How is it different from a normal authorization bug?
In a normal authorization bug the rule exists on the server and fails on one path, so a correct example sits next to the broken one. Here the server never had a rule at all. The backend is internally consistent and completely open, which is why reviewing the server code often turns up nothing that looks wrong.
Why do vulnerability scanners miss it?
Because there is no malicious input and no signature to match. The request is well formed, carries a real session, and targets a documented endpoint, and the server answers it happily with a 200. Finding the bug requires knowing what the application is supposed to charge for, which is a question about intent rather than about payloads.
How do you prevent a client side paywall bypass?
Enforce entitlement on the server for every request that returns paid data or performs a paid action. Never send data the user is not entitled to and mask it later, derive the plan from your own store rather than from the request, check entitlement in the same place you do the action, and test every premium route directly with a free account’s session.
Put an autonomous researcher on your own systems
UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.
