Home/Journal/Pattern
Patternsecretsapi-keysclient-security

The Secret in the Page Source

A live key shipped in a JS bundle, an APK, or a public repo is public. Here is why hardcoded secrets keep leaking and how to keep them server-side.

Rahul Dharan··8 min read

What this is

Anything you ship to the browser or the app store is public. This pattern is the live secret that gets shipped inside a client-reachable artifact - a web JavaScript bundle, a mobile app package, or a public code repository - and frequently with no restriction on how the key can be used once found. The secret is not hidden by being minified, bundled, or buried in a config file. It is distributed to everyone who loads the page or installs the app, and read in minutes by anyone who looks.

The severity turns on what the leaked key can do. A key that is meant to be public and is properly restricted is a non-event. The dangerous cases are the unrestricted ones: a third-party maps or payments key with no usage cap, which lets a stranger run up the bill or move funds, and the “sample” or default signing secret left in from a tutorial, which lets anyone forge tokens the server will trust. In both, the key was powerful and nothing constrained it.

Why it keeps happening

Getting a key into working code is the fast path, and the fast path is the one under deadline. During development, the difference between “the key lives on the server and the client asks the server” and “the key is right here in the client where I can see it work” is several hours of plumbing. The second option compiles, runs, and ships. It also publishes the key, but nothing in the build tells you that.

The mental model is the problem. A minified bundle looks opaque, so it feels private. A mobile binary feels like a sealed box. Neither is. Minification is not encryption, and an APK is a zip file. The gap between “looks hard to read” and “is readable by anyone with the standard tools” is where these keys sit.

AI-assisted development sharpens the edge. A generated snippet that wires up a maps widget or a payment flow will happily inline a key to make the example run, and a fast-moving team wires the example into the product. Public repositories add a second leak surface: a key committed once lives in the git history forever, and it takes only one push to a public repo, or one exposed .git folder, to hand it to whoever is scanning. Automated scanners crawl new public commits within minutes of a push.

How the attack works

Finding a shipped secret is not an attack so much as a search:

  1. Pull the artifact. Download the site’s JS bundle, unpack the mobile app, or clone the public repository - each is available to anyone.
  2. Grep for the shapes. Keys have recognizable formats and variable names. A search across the artifact surfaces them in seconds, and open-source scanners do this at scale automatically.
  3. Test the restriction. Try the key from an unexpected origin, IP, or scope. If it works from anywhere, it is unrestricted, and the leak is a live capability rather than an inert string.
  4. Use what it grants. A signing secret forges tokens. An unrestricted payment key moves or spends money. An unrestricted maps key runs up metered billing. A gateway credential reaches whatever the gateway fronts.

The work is trivial by design. There is no exploit chain, no timing, no bypass. The secret was published; the attacker only had to read it.

What it looks like in the wild

We have found this across very different products, which is what marks it as a pattern rather than a one-off:

The consistent theme: the secret was never stolen in any dramatic sense. It was shipped. The artifact that carried it was designed to be distributed, and the key went along for the ride.

How to tell if you are exposed

A short self-check any team can run today:

  1. Grep your own bundle. Download your production JS bundle and search it for key-shaped strings and secret-like variable names. If you find a live key, so has everyone else.
  2. Unpack your app. Pull your own APK or app binary, extract the config, and read the strings. Treat anything you find there as public.
  3. Search your git history, not just the tip. A key removed in a later commit still lives in history. Scan the full history of every repo, and confirm none is public that should not be.
  4. Classify every client-side key. For each key that legitimately reaches the client, confirm it is restricted by referrer, IP, scope, or spend. Any powerful, unrestricted key in a client artifact is a finding.

How to fix it

  1. Keep secrets server-side. The default is that the key lives on your backend and the client calls your backend, which uses the key. The client never holds the secret.
  2. If a key must reach the client, restrict it hard, and treat it as public. Lock it to a referrer, an IP range, a scope, and a spend policy. Assume it will be read, and make sure that being read grants nothing dangerous.
  3. Rotate anything that leaked. A secret that shipped is burned. Rotating it is the only real remediation; removing it from the next build does not un-publish the copies already distributed.
  4. Scan bundles and repositories in CI. Fail the build on a detected secret, in both the client artifact and the source repo. Automated scanning is the only defense that keeps pace with automated leaking.
  5. Never ship sample or default secrets to production. Treat every placeholder signing key and boilerplate credential as a release blocker until it is replaced with a real, secret value.

The takeaway

If a secret is in something you ship, it is not a secret. A key in a JS bundle, an app package, or a public repo is published the moment the artifact is, and a powerful unrestricted key published that way is a live capability handed to whoever looks. Keep secrets on the server, restrict the ones that must go out, and scan everything you ship.

This pattern recurs across our engagements. Two instances are written up in full: The Payment Key Anyone Could Spend and The .git Folder That Leaked the CRM.

Frequently asked

Why is a secret in a JavaScript bundle a vulnerability?
Anything shipped to the browser is readable by anyone who opens developer tools or views the source. A JS bundle is downloaded to every visitor's machine, so any key inside it is effectively published. The same is true of a mobile app: the APK or binary can be unpacked and its strings read. If a live secret is in there, it is not hidden, it is distributed.
What is the difference between a public key and a secret in client code?
Some keys are meant to be public and are safe in the client only when they are restricted - locked to a referrer, an IP range, a scope, or a spend policy. A secret is a key that grants real power with no such restriction: a signing key, an unrestricted payment or maps key, a gateway credential. The danger is not that a key reaches the client, it is that a powerful, unrestricted one does.
What is the risk of leaving a sample or default secret in production?
Framework tutorials and boilerplate ship with placeholder signing secrets meant to be replaced. If that sample value survives into production, the signing key is public knowledge, so anyone can forge valid tokens and impersonate any user. It is one of the most severe leaks because the secret was never secret to begin with.
How do you stop secrets from leaking into client code?
Keep secrets server-side and have the client call your backend, which holds the key. If a key genuinely must reach the client, restrict it hard - referrer, IP, scope, spend cap - and treat it as public. Scan both your JS bundles and your code repositories in CI so a committed or bundled secret fails the build, and rotate anything that has already shipped.

This is one finding from a harness that runs continuously. See how Greywatch finds, proves, and fixes them.

How it works →