Index
  • bug-bounty
  • web-security
  • api
  • business-logic

How an LLM helped me break a national newspaper paywall

How AI helped me move faster on a paywall bypass by turning frontend clues into the right API path.

···

October 2025 log. Big national newspaper, broken paywall, and one of the clearest times an LLM genuinely helped me move faster.

Context

News paywalls often look simple from the outside. You click an article, the page loads, and somewhere in the background the site decides whether you are premium or not. In reality that decision is usually spread across several moving parts: the main website, an auth or gateway API, a user session, and some cookie or entitlement object the frontend treats as truth.

That architecture is not a problem by itself. The problem starts when the premium cookie is no longer just the cached result of a server-side check, but becomes the thing the site actually trusts.

That is what I found here.

Minimalist illustration placeholder: article page, auth API, and premium cookie flow on a dark background

Where the LLM helped

I did not start with a model. I started the normal way: public JavaScript, DevTools, route-shaped strings, object names, whatever the bundle already leaked about auth. That was enough to see a gateway-style API, app-level credentials for the public frontend, and a paywall cookie object showing up on signup responses. What I still did not have was the one endpoint name that completed the story.

That is the kind of gap where a current LLM is already useful. Not because it invents bugs from thin air, but because it is fast at turning real fragments into plausible siblings: routes, helper names, internal-sounding paths that match the same style. I fed it what I already had and asked for candidates around accounts, previews, entitlements, and cookies. Most of it was noise. One line was the route that mattered.

People usually argue about this in the wrong way. It is not useless, and it does not replace you. You still own the target, the traffic, and the verification. The model just widens the search. On this program, I would not have tried that path as early without it.

The actual bug

Once I had the right route, the whole trust chain became visible.

The frontend shipped application credentials in client-side JavaScript. That meant I could authenticate to the gateway API as the public app and obtain an app token. With that token, I could create a normal account through the same API and receive a user token in return. The signup response also returned a paywall cookie object, which was the first real clue. It showed me the exact kind of artifact the frontend was using to model access.

That told me I was not dealing with some hidden premium check deep in the article renderer. I was dealing with a site that passed access state around as a reusable object.

The LLM-suggested route turned out to be a user-scoped helper that returned the same kind of paywall cookie, except now it let the caller ask for a stronger entitlement level than a free account should ever be able to mint. At that point the bypass was straightforward: set the returned cookie on the main domain, reload a premium article, and the page behaved as if the account had a subscription.

Technically, that is what made the bug interesting to me. The real issue was not just the exposed app credentials. It was that a normal account could obtain a premium-grade paywall cookie, and the website treated that cookie as sufficient proof of entitlement.

The signup response returning a baseline paywall cookie made the rest much easier to reason about. It was the same object model the whole way through, just with a different access value. Free and premium were not separate mechanisms. They were different states of the same artifact.

Later on I found a second public app identity in another bundle that could reach the same auth surface, which made it obvious this was not a one-off leak but a broader trust problem in the way the paywall stack was designed.

What I actually do on paywalled products

The step people skip is also the one that saves the most time: treat the shipped JavaScript like source code. Pull the big bundles from whatever is in scope, open them in an editor or ripgrep them, and look for hosts, path-looking strings, auth words, cookie names, anything that tells you how the frontend expects to talk to the backend. Bundles are usually minified, so when one line is unreadable I paste a slice through an online beautifier like beautifier.io and work from the formatted output. You are not hunting for a secret. You are building a map of what the client already believes is true.

After that I stop reading and start sending traffic. Signup, login, trial buttons, loading a locked page, whatever is cheap. I watch responses and storage the same way I would for any other app. If premium access shows up as a cookie or a header, I copy it, replay it, strip it, swap sessions, and see what the server actually enforces.

If the product really is cookie-driven, I also spend a few minutes on isolation. I take whatever blob unlocked premium for user A and I try to use it while logged in as user B in a clean profile. Sometimes you get nothing. Sometimes you get a second bug: the same entitlement travels across accounts, or the cookie is not bound to the right identity, or everyone shares one server-side slot. It is not always the same impact as a minting route, but it is the kind of check that is easy to run and annoying to explain if you never tried it.

When the bundle already hints at extra authenticated routes, I chase those too, but the order is always read what shipped, mirror that traffic honestly, then get weird.

How I use an LLM in that loop

I only paste material I already took from the target: a chunk of bundle, a hostname, a path prefix, keys from a JSON body. I ask for more strings in the same naming style, not for new domains or magic URLs. The model outputs candidates, I send the requests, and I throw away almost all of them. If something comes back with the same kind of access object but a different level or field, that is when I slow down and write it up properly.

I still like proving the first weird step in DevTools when I can. If it works there, it will work in a proxy, and I am not stuck arguing with myself about whether the chain is real.

Terminator

Talk, quote, still

Current LLMs are better vulnerability researchers than I am.

That line is on a slide in this talk. Loud claim, not a law for every engagement, but it lines up with what happened here: the model did not validate the bug, it just surfaced a path I was slow to guess.

I still agree with a lot of the talk’s angle. On boring string work and naming-shaped search, current models are already strong.

Still from the same talk:

Conference slide: LLMs and vulnerability research

Scope and disclosure

This was reported through the program, accepted, and written here in a deliberately blurred form.

Bye