API Keys Leaked in Frontend Code
An API key leaked in frontend code is a secret credential (database token, payment secret key, cloud key, or AI provider key) that gets compiled into the JavaScript bundle or source maps shipped to every visitor's browser, where anyone can read it with browser dev tools. Unlike a publishable key meant for the client, a secret key in the frontend grants attackers the same backend privileges your server holds.
The browser is not a vault. Every line of JavaScript you ship is downloaded, cached, and fully readable by anyone who opens dev tools, and that includes any secret a build pipeline accidentally inlined. The danger is rarely a developer typing a password into a button handler; it is the quiet machinery of modern bundlers folding an environment variable into a public asset, or a deploy that forgot to strip source maps. With AI coding assistants now scaffolding full-stack apps in minutes, a key meant for the server side increasingly lands in the client bundle before anyone reviews it. This guide untangles which keys are safe to expose, which are catastrophic, and how to prove which kind you actually shipped.
Reviewed by Ameya Lambat
Security Research Contributor, Legba
Reviewed 2026-05-28 · Updated 2026-05-28
What it is
API keys leaked in frontend code are sensitive credentials that have been embedded, intentionally or accidentally, into assets that the browser receives: minified JavaScript bundles, inline <script> blocks, framework runtime config, or source map (.js.map) files left in production. The critical distinction is between a publishable key and a secret key. A publishable key (for example a Stripe publishable key, prefixed pk_, or a Firebase web config) is designed to be public and constrained by server-side rules; exposing it is expected. A secret key (a Stripe sk_ key, an AWS secret access key, a database connection string, an OpenAI API key, or any token that authenticates as your backend) is account-level credentials. When build tools inline a variable like REACT_APP_STRIPE_SECRET or a non-public environment value, that secret compiles directly into the JS that every user downloads, making it visible to anyone who reads the bundle or network response.
A leaked secret key is a pre-authenticated door into your backend, and the loss is measured in money and data, not inconvenience. A leaked Stripe sk_ key lets an attacker pull customer records, issue refunds to themselves, and create charges; a leaked cloud key can spin up infrastructure billed to you or read your storage buckets; a leaked database or AI-provider key drains data and budget at machine speed. GitGuardian's State of Secrets Sprawl 2024 counted 12.8 million new secrets exposed publicly in 2023, and found that 90 percent of valid leaked secrets stayed active at least five days after the owner was warned, so the window attackers exploit is wide open by default. Because the secret is in a static asset, harvesting is automated and silent: there is no failed-login alarm, just a credential working exactly as designed for the wrong person. For a security team, the job-to-be-done is not a scanner alert that a string looks key-shaped; it is a confirmed, evidenced finding showing the key is live and what it unlocks.
At a glance
How attackers find and exploit it
- Enumerate the target's web properties and download every JavaScript asset referenced by the pages, including chunked and lazily loaded bundles, vendor files, and any .js.map source maps still served in production.
- Run high-entropy and pattern-based secret scanners (regex for sk_live_, AKIA AWS prefixes, AIza Google keys, ghp_ tokens, JWTs, and connection-string shapes) across the downloaded bundles and beautified source maps to surface candidate credentials.
- Beautify and de-minify the code, or reconstruct original source from leaked source maps, to recover variable names, internal API endpoints, and comments that reveal which service each key authenticates to.
- Validate each candidate live by calling the corresponding provider's lowest-privilege endpoint (for example a balance, account, or whoami call) to confirm the key is active and to fingerprint its scope without triggering obvious alarms.
- Pivot from a confirmed secret key to its full blast radius: exfiltrate customer or PII records, issue fraudulent transactions or refunds, provision cloud resources for cryptomining, or abuse a paid AI/API quota until the bill or the data loss is noticed.
- Automate the whole pipeline against many targets and re-scan periodically, since redeploys frequently reintroduce a rotated-but-re-leaked key or expose a new one.
How to detect it on your surface
- Inventory every public-facing web app and single-page application, then crawl each one to collect the full set of JavaScript bundles, dynamically imported chunks, and any source map files served alongside them.
- Scan those collected assets with a secrets detector that combines provider-specific patterns and entropy analysis, and treat any non-publishable key shape (sk_, AWS secret keys, private connection strings, bearer tokens) as a priority hit.
- Check whether .js.map source maps are reachable in production by requesting the .map URL referenced in each bundle's sourceMappingURL comment; recoverable source maps dramatically widen what an attacker can read.
- Audit your build configuration and environment-variable naming to confirm only intentionally public variables (NEXT_PUBLIC_, REACT_APP_ prefixed and reviewed) reach the client, and that nothing secret is being inlined at build time.
- Diff each new production deploy against the previous one for newly introduced credential-shaped strings, since regressions reintroduce secrets even after a clean-up.
Detection signals
- A string matching a known secret prefix in a served bundle, for example sk_live_ or sk_test_ (Stripe secret), AKIA followed by 16 uppercase alphanumerics (AWS access key id), AIza (Google API key), ghp_/github_pat_ (GitHub tokens), or xoxb-/xoxp- (Slack tokens).
- A reachable .js.map file (HTTP 200 on the URL named in a //# sourceMappingURL= comment) that reconstructs original module paths, comments, and inlined config.
- High-Shannon-entropy alphanumeric literals assigned to variables or object keys named secret, apikey, token, password, privateKey, or connectionString within minified code.
- Database or message-broker connection strings (postgres://, mongodb+srv://, amqps://) or full JWTs with decodable backend-scoped claims embedded in client assets.
- Provider validation success: the candidate key returns HTTP 200 from a benign account/identity endpoint rather than 401/403, confirming it is live.
Validate before you report
- Extract the exact candidate string with its surrounding context (file, variable name, nearby endpoint) so the finding identifies the specific service the key belongs to, not just a generic match.
- Classify the key as publishable or secret using the provider's own scheme (for example pk_ vs sk_ for Stripe, or whether a Firebase value is the intended public web config), so expected-public keys are not reported as criticals.
- Confirm the secret is live by issuing a single low-privilege, non-destructive call to the provider (account/whoami/balance read) and recording the authenticated response as evidence, without making any state-changing request.
- Determine effective scope and blast radius from the validation response or key metadata: is it a restricted/scoped key, or an unrestricted account-level secret that can read data and move money?
- Capture reproducible evidence: the source URL of the asset, the byte offset or line of the match, the masked key, the deploy/build timestamp, and the validation response, so the owner can act and revoke immediately.
What looks like this but isn't
- Confirm publishable/public-by-design keys are not flagged as secrets: a Stripe pk_ key, a Firebase web apiKey, a Mapbox public token, or a Sentry public DSN are intended to be in the client and constrained server-side, so they are not a leak on their own.
- Rule out test, placeholder, example, or expired credentials (sk_test_ values from docs, obvious dummy strings, or keys that return 401 on validation) which match a pattern but unlock nothing.
- Check that a high-entropy string is actually a credential and not a cache-busting hash, content-hashed filename, build id, integrity hash, or UUID that merely resembles a key.
- Verify the key has not already been rotated/revoked since the asset was cached; a stale CDN copy can surface a string the provider no longer honors.
Remediation
- Revoke and rotate the exposed secret immediately at the provider before doing anything else, and assume it has been harvested the moment it was reachable in a public bundle.
- Review provider audit logs for the exposure window to detect any unauthorized use, then contain and remediate downstream impact (block fraudulent charges, lock affected resources, notify stakeholders if data was accessed).
- Remove the secret from client code and move all backend calls that need it behind a server-side route, API gateway, or serverless function, so the browser never receives the credential.
- Store secrets only in a dedicated secrets manager (AWS Secrets Manager, Google Secret Manager, Azure Key Vault, or HashiCorp Vault) and inject them at runtime on the server, per the OWASP Secrets Management Cheat Sheet, rather than in source or build-time client variables.
- Where a value genuinely must reach the client, replace the secret key with a publishable or restricted/scoped key that has only the minimum permissions needed (for example a Stripe restricted API key).
- Disable production source maps or restrict them to an authenticated error-monitoring upload (so they never serve publicly), and verify the deployed bundles no longer reference reachable .map files.
- Purge the secret from version-control history (it persists in old commits and forks even after a clean working tree) and from any CDN caches still serving the old asset.
Operational checklist
- Run automated secret scanning in pre-commit hooks and CI so credential-shaped strings are blocked before they reach a branch, not discovered after deploy.
- Enforce an environment-variable naming convention where only an explicit public prefix (NEXT_PUBLIC_, REACT_APP_) is allowed in the client, and fail the build if any other secret is inlined.
- Make production source maps non-public by default in your bundler config and add a deploy check that fails if a reachable .js.map is served.
- Adopt least-privilege, scoped, and short-lived keys with automated rotation for every external service, so any single leak has bounded impact and a short life.
- Continuously monitor your live external attack surface for newly exposed secrets after every deploy, since regressions reintroduce keys that earlier scans cleared.
- Maintain a documented, rehearsed key-revocation runbook so a confirmed leak is rotated in minutes, not the five-plus days that leaked secrets typically stay active.
What to do next
A secret key sitting in your JavaScript bundle is already public; the only open question is whether an attacker or your security team finds it first. The fix is fast once the finding is confirmed: rotate the key, move the call server-side, and lock down source maps. The trap is the unverified alert, a scanner flagging a harmless publishable key while the live secret key two chunks over goes unnoticed. Start now by crawling one production app, pulling its bundles and source maps, and validating every credential-shaped string against its provider so you know exactly which keys are real, live, and worth your next hour.
Methodology
Each finding-type guide is built from Legba Recon's real detection and validation logic, reviewed by a named security contributor, and cited against primary sources such as OWASP, CISA, NIST, and MITRE. We update pages when the underlying guidance changes. See our contributors and company.
FAQs.
References.
- 01
- 02
- 03API8:2023 Security MisconfigurationOWASP API Security Project
- 04API keys best practicesStripe
- 05The State of Secrets Sprawl 2024GitGuardian
