Join our Newsletter — 33% off our NHI Course
Home› Guides› Secrets in Frontend JavaScript Bundles and Source Maps:…
Guide Secrets Management

Secrets in Frontend JavaScript Bundles and Source Maps: Find, Rotate and Keep Keys Server Side

PDF for newsletter subscribers · free to read online

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 13 November 2026 14 min read
On this page

Any value a frontend build reads from an environment variable can end up as plain text in the JavaScript every visitor downloads, and a published source map can hand over the original source, comments included. Modern frameworks make this easy to do by accident: a variable with the right prefix is copied into the bundle at build time, and from then on it is public. This guide explains how build-time inlining and source maps expose keys, how to tell a publishable key from a real secret, how to find and rotate what has leaked, and how to move secret calls behind a server. It is for frontend and platform engineers, application security teams and anyone triaging an "API key exposed in JavaScript" finding. For how environment variables and .env files behave on servers, see the Environment Variables and .env Files guide; this page covers only the browser side.

Key takeaways

  • Prefixed means public. Vite exposes VITE_ variables, Next.js inlines NEXT_PUBLIC_ variables and Create React App embeds REACT_APP_ variables into the bundle. Each framework's documentation says not to put secrets in them.
  • Build-time values are frozen and copied everywhere. An inlined key sits in every cached copy, CDN edge and archived snapshot of the bundle, so deleting it from the code does not withdraw it. Only revocation does.
  • Source maps can publish your source. A full map with sourcesContent carries original files, comments and dead code. Keep production maps off the public web server or upload them privately to error tooling.
  • "Public" keys still need restrictions. Publishable keys (Stripe pk_, Firebase, Maps browser keys) are designed for the browser, but only when restricted by API, referrer and quota. Enabling a new API on the same project can turn them into real credentials.
  • Anything that needs a secret runs on a server. Use a backend, serverless function or backend for frontend that holds the key and exposes only the narrow operation the browser needs.

How secrets get into the bundle

Bundlers replace references to environment variables with literal values when the build runs, so the bundle contains the value itself, not a lookup. Next.js describes this as replacing all references with a hard-coded value that is "inlined into any JavaScript sent to the browser", and notes the value is frozen at build time. Vite says its variables are statically replaced at build time. The prefix is a safety gate: variables without it stay out of client code, but the gate only works if nobody puts a secret behind the prefix.

Framework or toolWhat reaches the browserMechanismWhat the documentation says
Create React AppVariables prefixed REACT_APP_ (plus NODE_ENV)Embedded during the build into static files"Do not store any secrets (such as private API keys) in your React app"
Next.jsVariables prefixed NEXT_PUBLIC_; others stay on the serverInlined at next build; dynamic lookups such as process.env[name] are not inlinedServer code can read unprefixed variables at runtime during dynamic rendering
ViteVariables prefixed VITE_, or the custom envPrefixStatically replaced on import.meta.env at build"VITE_* variables should not contain sensitive information such as API keys"; use a backend or serverless function instead
webpack DefinePlugin and similarWhatever the config maps inText replacement at compile timeNo prefix gate: a config that maps all of process.env exposes everything
Hard-coded config filesAny constant imported by client codeBundled like any other moduleTreated as source code, so CWE-798 and CWE-540 apply

Three less obvious routes cause many findings: a shared config module imported by both server and client code, which pulls the server values into the client graph; a CI pipeline that injects every secret into the build environment and a prefix rename that later exposes one; and server-side rendering that serialises configuration into the HTML page for hydration.

Source maps: what they reveal and how to ship them safely

A source map links minified code back to the original files. If it includes sourcesContent, it contains the original source text, including comments, unused branches and any constants that minification renamed but did not remove. CWE-540, Inclusion of Sensitive Information in Source Code, and its child CWE-615, sensitive information in comments, describe the result. The map also reveals internal API paths, feature flags and file structure useful for reconnaissance.

SettingEffectSafe production use
Vite build.sourcemap: false (default)No map generatedSafe; no mapped stack traces
Vite true / webpack source-mapSeparate .map file plus a reference comment in the bundleOnly if the server blocks .map requests; webpack warns to disallow access for normal users
Vite 'inline'Map appended to the bundle as a data URINot for production: every visitor downloads it
Vite 'hidden' / webpack hidden-source-mapMap generated, no reference commentFor error tooling; webpack says not to deploy the file to the web server
webpack nosources-source-mapMap without sourcesContentCan be deployed; still exposes file names and structure
Next.js productionBrowserSourceMaps: trueMaps written next to the JavaScript and served automaticallyOff by default; enable only with server-side blocking or private upload

The usual pattern is to generate hidden maps, upload them to the error-monitoring service during the build, then delete them from the output folder before deployment. Sentry's Vite plugin documents a filesToDeleteAfterUpload option for this and warns that generated maps may otherwise leak source. Add a web tier rule that returns 404 for *.map as a second layer; OWASP ASVS 5.0 V13.4.7 asks that the web tier serve only specific file extensions to prevent source leakage.

Publishable key or real secret?

Not every key found in JavaScript is an incident. Some are designed for the browser, but each depends on restrictions to stay safe. Products are listed alphabetically; the order is not a ranking.

CredentialBrowser safe?Condition or action
Analytics or tag IDsYesIdentifiers, not credentials; watch for abuse through quota and allowed domains
Firebase API keyYes, if restricted to Firebase APIsProtection comes from Security Rules and App Check, not secrecy; see the Firebase Security Rules Guide
Gemini Developer API and other AI provider keysNoFirebase docs say never to allowlist the Gemini Developer API on a publicly accessible key; call from a server
Google Maps JavaScript API keyYes, with restrictionsApply HTTP referrer and API restrictions, one key per app; Google says web service keys should not be publicly exposed
OAuth client secret, webhook signing secret, database or cloud keysNoA browser app is a public client; use PKCE or a backend for frontend
Stripe publishable key pk_live_YesStripe states it can be included in front-end code; it cannot create charges or read account data
Stripe secret sk_ or restricted rk_ keyNoServer only, with access policies; rotate if exposed

The Google API keys exposure shows why the "safe to expose" label is conditional: researchers found 2,863 live keys with Gemini access in November 2025 web archive data, because enabling the Gemini API on a project extended keys that had been embedded in websites as public identifiers. Treat enabling a new API as a change to every key in that project.

Finding exposed keys

  • Scan the build output, not just the repository: run a secret scanner over dist/, build/ or .next/static/ in CI after the build and fail the pipeline on a verified hit. TruffleHog's filesystem mode scans directories and verifies findings against the provider by default. Repository scanning misses values injected from CI variables.
  • Scan what is live: crawl production and staging, download every script and any reachable .map file, and scan them. Include old bundle hashes still cached on the CDN.
  • Check the prefix list: list every VITE_, NEXT_PUBLIC_ and REACT_APP_ variable across projects and CI settings, and confirm each one is meant to be public. A name containing SECRET, PRIVATE, PASSWORD or TOKEN behind a public prefix is a finding by itself.
  • Check maps: request <bundle>.js.map for each script and look for sourceMappingURL comments in production bundles.
  • Use the provider's view: key usage logs, referrer reports and unexpected billing often reveal a leaked browser key before a scanner does.

Attackers use the same approach: searching victim-owned websites and open sources (ATT&CK T1594 and T1593) and harvesting credentials in files (T1552.001). For triage of scanner output, see Developer Secret Scanning Triage.

Rotating a key exposed in a bundle

A redeploy without the key does not end exposure. Old bundles stay in browser caches, CDNs, web archives and scanners' collections, and OWASP's Secrets Management Cheat Sheet notes that restarting an application does not revoke stolen credentials.

  1. Classify: confirm whether the value is a publishable key or a secret, and what it can reach today, including any APIs enabled since it was created.
  2. Revoke and replace: OWASP's order is revoke, rotate, then remove from the exposed system. Where a provider offers an overlap, use it briefly: Stripe keeps the old key valid for up to 7 days after rotation unless you choose to expire it now.
  3. Move the call: if the key was a secret, ship the server-side replacement before or with the rotation, otherwise the next build reintroduces it.
  4. Purge: invalidate CDN caches for old bundles and maps and remove them from the origin.
  5. Review usage: check the provider's logs for calls from unexpected origins or volumes during the exposure window, and check for data accessed or charges incurred.
  6. Restrict the replacement: for publishable keys, apply API, referrer and quota restrictions before release.

Keeping keys server side

  • Thin proxy or serverless function: the browser calls your endpoint, which authenticates the user, enforces rate limits and calls the provider with a key held in a secrets manager. Vite's documentation points to a backend or serverless and edge functions for this.
  • Backend for frontend: for OAuth, keep tokens and client credentials on a server component and give the browser only a session cookie. See the OAuth for Browser-Based Applications and the BFF Pattern Guide.
  • Short-lived, scoped tokens: where the browser must talk to a provider directly (uploads, real-time services), have the server mint a token scoped to one user and one action that expires in minutes.
  • Runtime configuration for non-secrets: Next.js notes that build-time public values are frozen and suggests your own API for runtime values. Serving non-secret config from an endpoint also lets one image move between environments.
  • Restricted publishable keys: where a browser key is intended, use one key per application with API and referrer restrictions, as Google's Maps guidance recommends, and keep AI or admin APIs in a separate project.

Where identity fits

Every key in a bundle is a non-human identity with an owner, scope and lifetime, even when it is labelled public. Record it in the key inventory with its owning team, the APIs it can reach and its restrictions, and review it when the project's enabled APIs change. Keys that must exist in the browser should be scoped as identifiers; anything that authorises an action belongs to a server-side workload identity. See the API Key Management Guide.

How to implement: a phased approach

  1. Discover (week 1): scan live sites, CDN contents and build outputs; list every public-prefixed variable and every reachable source map.
  2. Contain (week 1 to 2): revoke and replace confirmed secrets, block *.map at the web tier and purge caches.
  3. Fix the build (weeks 2 to 4): set production maps to hidden or off, upload privately and delete after upload; stop injecting all CI secrets into frontend build jobs, giving them only an allowlist of public variables.
  4. Move calls server side (weeks 2 to 8): replace direct secret calls with proxies or a BFF, ship behind a feature flag, and keep the old path available for rollback until error rates are stable, with the old key already revoked.
  5. Prevent (ongoing): add a CI gate that scans build output and fails on verified secrets or on .map files in the deploy artefact; add a lint rule rejecting public-prefixed names that contain SECRET, PRIVATE or PASSWORD.

Decision guidance

  • Restrict or move? If the provider documents the key as browser safe and offers referrer and API restrictions, restrict it. If the key can read data, spend money without a cap or perform admin actions, move it behind a server.
  • Hidden maps or no maps? Teams with error monitoring should use hidden maps uploaded privately. Without monitoring, turn maps off in production.
  • Small teams: a managed serverless function in front of each third-party API is usually the fastest fix and needs no new infrastructure.
  • Large estates: standardise on a shared API gateway or BFF, central key inventory and a single CI scan step for all frontend builds, so each team does not solve it separately.

Detection and measurement

Log and alert on provider key usage from unexpected referrers, countries or volumes; on requests for .map files at the web tier; and on scanner hits in build output. Track:

  • Verified secrets in shipped bundles: target zero, measured by the post-build scan and the live-site crawl.
  • Publicly reachable source maps: count of production .map URLs returning 200.
  • Browser keys with restrictions: percentage of publishable keys with both API and application restrictions.
  • Time to revoke: hours from detection to revocation for keys found in bundles.
  • Frontend pipelines with build-output scanning: percentage of frontend repositories with the CI gate enabled.

Common failure patterns

  • Prefix used to "fix" a missing value: a developer renames API_SECRET to NEXT_PUBLIC_API_SECRET because the client could not read it, and the secret ships.
  • Removing the key from code and calling it fixed: the value stays valid and stays in cached and archived bundles until it is revoked.
  • Maps generated for error tooling and deployed anyway: the upload step works, but the delete step or the server block was never added.
  • Unrestricted "public" keys: browser keys left unrestricted gain new powers when another API is enabled on the project, as the Google API keys exposure showed.
  • Scanning only Git: values injected from CI variables never appear in the repository, only in the build output.
  • Server values serialised into HTML: server-side rendering passes a whole config object to the client for hydration, secrets included.

Framework mapping

FrameworkReferenceRelevance
OWASP ASVS 5.0V13.3.1Secrets must not be included in application source code or build artifacts
OWASP ASVS 5.0V13.4.2, V13.4.7Debug features off in production; web tier serves only specific file extensions
OWASP ASVS 5.0V14.2.1No API keys or tokens in URLs or query strings
NIST SP 800-53 Rev. 5IA-5(7), SA-11No embedded unencrypted static authenticators; developer testing
NIST CSF 2.0PR.PS-06, DE.CM-09Secure development practices; monitoring of software and runtime environments
MITRE CWECWE-798, CWE-540, CWE-615Hard-coded credentials; sensitive information in source code and comments
MITRE ATT&CKT1552.001, T1594, T1593Credentials in files; search of victim-owned websites and open sources

Practitioner checklist

  • List every public-prefixed environment variable and confirm each is meant to be public.
  • Give frontend build jobs only an allowlist of public variables, never the full secret set.
  • Scan build output in CI and fail on verified secrets.
  • Set production source maps to hidden or off, upload privately and delete them before deploy.
  • Block *.map requests at the web tier and purge old maps from the CDN.
  • Apply API, referrer and quota restrictions to every publishable key, one key per application.
  • Keep AI provider, admin and payment secret keys in a secrets manager behind a server-side call.
  • Revoke, not just remove, any secret found in a bundle, then review provider logs for the exposure window.
  • Re-check browser keys whenever a new API is enabled on their project.

Standards and references

Related NHI Mgmt Group resources: Environment Variables and .env Files · Client-Side Security Guide · Git Secret Scanning Guide · Generic API Key Detection Guide

PDF for subscribers

Take this guide with you

Every section, table and checklist in one PDF, with links to the sources and related guides. Subscribe to our weekly NHI & AI Identity Journal and we’ll email you a download link.

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 13 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org

PDF Downloads for Newsletter Subscribers

Get the guide

Enter your details and we’ll email you a download link.

The full guide is free to read online. The PDF is a thank you for subscribing.

Fill in all fields marked * and tick the PDF box to continue.