Your frontend is public by design. Every script your app ships is downloaded by every visitor, readable by every bot, and archived by services that never forget. Teams know this in principle. Then the build pipeline inlines an environment variable, and a secret that was supposed to live on the server goes out to the world inside a minified JavaScript bundle.
In our latest batch of assessments, 33 percent of the applications we attacked shipped at least one secret in their public client code, and 38 percent exposed public source maps or build metadata that handed us a readable copy of the application to go with it. This is the fourth finding in our field series, after endpoints that skip authentication, exposed internal tools, and forgotten staging environments. This one requires the least skill to find. An attacker does not need to break in. They need to view source.
What a bundle leak is
A bundle leak is a secret, a credential, or internal detail embedded in client-side code that every visitor downloads. MITRE tracks the underlying pattern as CWE-798, Use of Hard-coded Credentials. The bundle is the compiled JavaScript, CSS, and HTML your build tool produces. Anything that lands in it is public property, no matter how private the variable name sounds.
Some keys are public on purpose, and fine: analytics project IDs, publishable payment keys, error-reporting DSNs. The finding is everything else. Signing keys. License keys. Storage tokens with no restrictions. Internal hostnames. Full source maps.
What an attacker finds in the bundle
No exploit, no access, no account. One download and a search. Real findings from recent engagements, anonymized:
- An AI company shipped the key that signs its email unsubscribe tokens in its public page source. Anyone could forge a valid token for any address and unsubscribe, or re-subscribe, strangers at will. The key was the only thing standing between the internet and that feature, and it was not standing anywhere.
- A document-handling service embedded a file-storage key with no usage policy. Customer uploads, including sensitive personal documents, were publicly readable on the storage provider's CDN to anyone who had the URL pattern, and the key made the pattern trivial to work out.
- A healthcare platform inlined two commercial license keys, one staging and one production, in its public bundle. Free use of paid software, on their license, attributable to their account.
- A scheduling platform leaked payment-customer IDs and internal credential IDs on every public booking page. Not directly exploitable, but a gift for anyone correlating breaches or building a targeted phishing list.
- One production bundle carried an analytics token, a feature-flag client ID, a development-environment token URL, and the internal backend hostname map in a single file. The reconnaissance phase of an attack, pre-completed.
- Another company's public chunks disclosed its complete internal route table, five error-tracking DSNs, and the names of its internal application family. Its private architecture, published by its own build.
The search itself is a one-liner:
curl -s https://app.example.com/main.js | grep -oE 'sk_[a-zA-Z0-9]{16,}'
If that returns anything, the secret is already in every web archive that crawled the page. Removing it from the next deploy does not take it back.
Worth saying: the server-side security at most of these companies held up. The leak was not in the API. It was in the artifact nobody reviewed.
Why this is a real problem
A leaked key is not a weakness that might be exploited. It is a working credential that already works, usable from anywhere, attributable to your account.
The impact scales with what the key opens. A signing key means forgery: tokens, links, and messages your backend trusts because your backend made the rule. An unrestricted storage key means your customers' files are public. A license key means someone else spends your license. And the "harmless" keys, the analytics tokens and DSNs everyone calls public-by-design, leak the internal structure an attacker needs for the next step: hostnames, service names, environments, route tables.
Source maps multiply all of it. With maps enabled, minified production code unfolds back into readable source, complete with original variable names, comments, and file paths. We have reconstructed authentication flows from source maps alone, then used what we learned to find the endpoints we reported in the first post in this series.
Why teams ship secrets in the frontend
The reason is convention, not carelessness. Modern frameworks make it easy to inline configuration at build time, and the naming rules, the prefixes that mark a variable as public, are conventions rather than enforcement. Miss the prefix, or add a variable before the rule existed, and the secret ships. Nobody gets an error. The app works fine. The leak is silent.
Source maps stay on because someone needed them for a debugging session months ago. And most secret-scanning tools watch the repository, not the build output. The variable looked safe in the .env file. What the bundler did with it never got scanned.
Finally, "public by design" becomes a habit. Once a team decides analytics keys are fine to expose, the review of what else is in the bundle gets looser every release.
How to keep secrets out of the bundle
The fix is a boundary enforced by tooling, not intentions. Four steps.
Step one. Search your own build output. Download your production JavaScript and grep it for the shapes of secrets: sk_, key, token, secret, BEGIN PRIVATE KEY, your cloud provider's key formats. Do it for every chunk, not just the main one. This is exactly what an attacker does in the first ten minutes, and it is the fastest security test you will ever run.
Step two. Sort every value: publishable or secret. Publishable means its exposure changes nothing an attacker can do. Everything else moves behind the server. Signing happens server-side. Admin and storage operations go through your API, which holds the credential and enforces authorization. The client keeps only what it can safely lose, because it has already lost it.
Step three. Rotate what has leaked. Removing a key from the next release is not remediation. The bundle is in web archives, browser caches, and CDN edges. Assume every shipped secret is known: revoke it, issue a replacement, and add usage restrictions, such as domain allowlists and permission scopes, so a future leak does less.
Step four. Gate source maps and scan every build. Disable public source maps in production, or serve them only to authenticated staff. Add secret scanning to CI against the build artifacts themselves, not just the source. Then re-run the check after releases, because the bundle changes with every deploy. Continuous beats annual here too, and it is how we work.
Frequently asked questions
What secrets are acceptable in frontend code?
Only values that are public by design and worthless to an attacker on their own: analytics project IDs, publishable payment keys designed for client use, and error-tracking DSNs. Even these should carry usage restrictions where the provider offers them. Anything that signs, authorizes, uploads, or bills belongs on the server.
Are publishable API keys a vulnerability?
Not by themselves. Providers design publishable keys for client-side use and expect them to be seen. The finding is when a key with real privileges, signing, unrestricted storage access, admin operations, appears in client code, or when a publishable key has no usage restrictions and can be abused for spam, quota theft, or injection into your analytics.
What is a source map, and why is it risky?
A source map is a file that lets browsers translate minified production code back into the original source for debugging. Publicly available source maps hand attackers your readable codebase: original variable names, comments, file paths, and logic. Debugging belongs in your error-tracking tool, which can use maps without publishing them.
How do I check my own JavaScript bundle for secrets?
Open your browser's developer tools, find the script URLs your app loads, download each one, and search for secret-shaped strings: key prefixes, tokens, private-key headers, internal hostnames. Repeat on staging builds and older chunks still served by your CDN. If you find a secret, treat it as compromised and rotate it.
What should I do when a secret leaks into a build?
Rotate first, remove second. Revoke the exposed credential and issue a new one, because archives and caches have already seen it. Then remove it from the bundle, find how it entered the build, and add a CI check that scans build artifacts so the same class of leak cannot ship again.
The point
We keep finding secrets in bundles because the bundle is the one artifact nobody reviews. The source gets reviewed. The infrastructure gets reviewed. The compiled file that actually ships to every visitor goes out unread.
If you want to know where you stand, it takes a few minutes. Download your own production JavaScript and search it for sk_, token, secret, and your cloud provider's key prefixes. Then do the same for your staging builds. Whatever you find, an attacker with the same ten minutes would find too.
And if you would rather have someone read your bundle the way an attacker does, and keep reading it every time it changes, that is exactly what we do.
Related reading: The Environment Everyone Forgets to Defend, where the staging keys in your bundle usually point. And Your Internal Tools Are Not Internal, the dashboards those leaked hostnames lead to.