4 min readRECON · SECRETS · WEB

Finding secrets in client bundles: grep the shipped code, then triage

Harvesting keys from front-end JS, sourcemaps, and committed .env files, and the part that matters more than finding them: knowing which key actually bypasses your security model and which is harmless by design.

The browser is an honest informant. Whatever logic runs client-side, you get a copy of, minified and bundled but fully present. So recon on any web target starts the same way: pull every script the app loads and treat it as a corpus to search. The naive version of this (grep for api_key) finds a hundred harmless strings. The version that pays is knowing which of those hundred strings actually breaks something, and most don’t.

Where the strings live

Four sources, in rough order of how often they pay out:

  • The JS bundles themselves. Minification renames variables, not string literals. An API key embedded at build time survives minification intact. Pull every .js the page references, including lazy-loaded chunks the SPA fetches after navigation.
  • Sourcemaps. A .map file next to a bundle reconstructs the original source, comments and all. Developers strip the bundle from production and forget the sourcemap, or ship it deliberately for debugging. Check for a //# sourceMappingURL comment at the end of each bundle, then fetch the map.
  • Committed .env files in deploy sources. Some deploy pipelines serve the build directory wholesale, and a .env, .env.production, or config.json rides along. Also worth checking: the same file in the public Git history if the deploy source is a repo.
  • Inline config blobs. A window.__CONFIG__ = {...} or a hydration payload in the HTML often carries the whole client config object, keys included.
bash
# pull every script the page loads, then search the lot
# grep the corpus for high-signal patterns, not just "key"
sk-[a-zA-Z0-9]{20,}        # generic secret-key shapes
eyJ[A-Za-z0-9_-]{10,}\.    # a JWT, often a long-lived service token
service_role               # the one that matters most, see below
-----BEGIN.*PRIVATE KEY    # an actual private key, ship-stopper

The triage that actually matters

Finding a key is the easy 20 percent. The reason most “I found an API key in the bundle” reports get closed as informative is that the key was designed to be public. The whole skill is sorting the harmless from the catastrophic.

The clearest example is the publishable-key versus service-role-key split that modern BaaS platforms hand teams. Both look like opaque tokens. One is on every page by design: it can only do what the database’s row-level security policies permit, so it’s effectively the identity of “an anonymous visitor.” The other, the service-role key, is built to run trusted server code and bypasses row-level security completely. A team that pastes the wrong one into their front-end config has shipped a key that reads, and often writes, every row in every table, no matter what their policies say.

So when I pull a key out of a bundle, the first question is which side of that line it sits on. The way you tell, without doing anything destructive: a publishable key, used against the data API, returns only what an anonymous user should see, because the policies still apply. A service-role key returns rows that should be invisible to anyone, because it ignored the policies. One read-only request that returns data an anonymous user provably cannot see is the entire proof.

How I run it

The flow that keeps this efficient instead of a string-grep swamp:

  1. Spider every script and map the SPA loads, plus the inline config. Build one searchable corpus.
  2. Grep for high-signal shapes (private keys, service-role markers, long-lived JWTs, cloud credential formats), not the word “key.”
  3. For each candidate, classify: public-by-design or privileged. Discard the former immediately.
  4. For a privileged candidate, do one non-destructive read that demonstrates access an unprivileged caller can’t have. Stop there.

That last step is what turns a grep hit into a report a triager can’t wave away. The transferable lesson is that secret-hunting is mostly a classification problem, not a search problem. The strings are easy to find; the judgement about which one bypasses the security model is the whole job, and it’s the difference between an “informative” close and a critical.