← Back to blog

The Unlocked Door: Client-Side Security Theater

When your paywall lives in JavaScript, it is not a paywall at all.

Over the years, I've seen the same pattern repeat across dozens of services: a slick demo page, a "try 5 times free" limit, and a JavaScript bundle that contains the entire enforcement logic.

Open DevTools. Delete a cookie. Unlimited access.

This post is not about any single company. It's about a pattern I see constantly: client-side enforcement protecting server-side resources. The door is locked, but the lock is on your side, and they handed you the key.


The Architecture

Here's the pattern, generalized from what I've observed:

The "Protection"

  1. User visits demo page
  2. JavaScript bundle loads
  3. Bundle contains trial logic: "if counter >= 5, show upsell"
  4. Counter stored in a cookie
  5. Each generation increments the cookie

That's it. That's the entire security model.

The Reality

// The "protection" is literally:
const count = getCookie('generation_count');
if (count >= 5) {
  showPaywall();
  return;
}
// ... proceed with API call
setCookie('generation_count', count + 1);

The server has no idea how many times you've used the demo. It just processes requests. The limit exists entirely in JavaScript that you control.


Three Ways Through the Unlocked Door

1. Delete the Cookie

The simplest bypass. Open DevTools, Application tab, delete the counter cookie. Refresh. You have 5 more tries. Repeat forever.

2. Modify the Bundle

Use a resource override extension to serve an edited version of their JavaScript. Find the trial logic, delete it. The check no longer exists in your runtime.

They ship you the code. Your browser executes it. You decide what it means.

3. Call the API Directly

This is where it gets interesting.

In the worst cases I've seen, demo pages fetch API tokens from public endpoints. No authentication required. The token is a real bearer credential for production infrastructure.

// Pattern I've observed - public token endpoint:
const response = await fetch('https://[demo-server]/token');
const { apiToken } = await response.json();
// apiToken is now a valid bearer token for production API

With that token, you can hit the API directly, bypassing the demo UI entirely. The "5 tries" limit? It only exists in the UI you're no longer using.


What This Enables

Attack Difficulty Impact
Unlimited personal use Trivial Lost conversion
API cost exhaustion Easy Financial
Shadow service Moderate Competitive
Capability enumeration Easy Intelligence

The shadow service scenario is the scary one. Someone could:

  1. Fetch fresh tokens programmatically (they expire hourly, but the endpoint is public)
  2. Proxy requests through their own backend
  3. Build a "free" version of the service
  4. Monetize it themselves

Technically trivial. Legally actionable. Ethically questionable. But the architecture makes it possible.


Why Companies Ship This Way

Before you judge too harshly, understand the tradeoffs:

These are reasonable product decisions. They're just not security decisions.

The problem is not that they chose convenience. The problem is when teams believe client-side enforcement actually protects anything. It doesn't. It guides honest users through a funnel. That's all it can do.


The Fix

If the resource is worth protecting, protect it on the server.

Short-term

Long-term

None of this is exotic. It's standard API security. The gap is that demo/trial experiences often get treated as "not real" when they're hitting real infrastructure.


The Broader Pattern

This is not unique to AI services. I see it everywhere:

The common thread: trusting the client. Assuming the code you ship will execute as written. Forgetting that the user's machine is hostile territory from your perspective.

The client is in the hands of the enemy. Never trust it.

This is the architectural reality of client-server systems. Your JavaScript is a polite suggestion. The user's browser is the interpreter. They decide what your bytes mean when they execute.


A Note on Responsible Research

The patterns described here are composites from observations over many years, across authorized assessments and publicly documented vulnerabilities. Nothing in this post describes active exploitation or unauthorized access.

If you recognize this pattern in your own infrastructure, the fix is straightforward. Happy to discuss.


Chris Aziz, Bombadil Systems