This page is available in English only.

Stop a paywall bypass in your shipped JavaScript

A paywall gate in the bundle is a named function returning a boolean, and flipping it is a one-line patch. What obfuscation and a per-build seed change about finding it and keeping the patch working.

A paywall or entitlement check that runs in the browser is expensive to find and expensive to patch only if you make it so. Minified output makes it neither.

Why a paywall gate is a one-line patch

In plain or minified output, a paywall gate is usually a named function that returns a boolean. Anyone reading the bundle can search for the word that names it: isSubscribed, hasAccess, checkEntitlement. Once found, the cheapest attack is a one-line patch that flips the return value.

That patch works on every user's copy of the file, and it keeps working on your next release if the file has not changed shape. The work of finding the gate gets paid for once and reused on every build after it.

What the shipped file looks like after a build

A default build renames identifiers and encodes string literals, so the function that used to be called isSubscribed no longer carries that name and the strings around it are no longer readable text. Searching the shipped file for the name or the message finds nothing to patch.

The property that matters most here is per-build polymorphism. The seed is new on every build unless you pin it, so a patch written against one release does not line up with the next one. An attacker who works out where the gate lives pays that cost again every time you ship.

Raise protection on the gate

The default preset already renames and encodes at a low complexity target. To raise the whole bundle a step:

$ npx afterpack@latest dist/ --preset=hard

To raise just the gating function without touching the rest of the bundle, mark it with a directive:

/* @afterpack preset=hard */
function checkEntitlement(token: string): boolean {
  return validateSubscription(token);
}
/* @afterpack end */

Directives take effect on Pro builds.

Check the gate came out protected

Open the Protection Map after a build and find the gating function. It should be coloured more strongly than the code around it, reflecting the higher complexity target you set for that region. If it looks the same as the rest of the bundle, read the build's diagnostics: a directive that was read but had no effect is reported there.

Where a client-side gate ends

Per-build rotation is the durable part of this. A new seed every release means the work of locating and patching the gate never carries forward, so a patch, script or userscript written against one build stops applying to the next one. A static reader working through a single build can still recover an equivalent computation, since the gate has to run correctly in the browser, so pair the client-side check with a server-side entitlement call for anything that gates revenue.

Next