Secrets in Frontend JavaScript: The Bundle Leak
API keys and tokens baked into client-side bundles are public the moment you ship. We cover what leaks, how recon finds it, and how to keep secrets server-side.
Anything shipped in a frontend bundle is public — the browser downloads and parses your JavaScript to run it, so every constant, comment, and config value is readable by anyone who opens DevTools. To a researcher doing recon, the bundle is a free credential dump waiting to be parsed. This is how to find the secrets and prove they work.
Finding it
Secrets keep landing in client code because a build-time environment variable feels private even when it is inlined. Your recon job is to collect every script the app loads and scan it.
What actually leaks:
- Third-party API keys with broad scopes (payment, email, maps, analytics).
- Cloud access keys mistakenly inlined into config.
- Internal API hostnames, feature flags, and undocumented endpoints.
- Source maps that reconstruct the original commented source.
The pipeline:
- Crawl the site and collect every JS asset, including lazily-loaded chunks.
- Run secret-scanning regexes (key prefixes, high-entropy strings) over the contents.
- Pull source maps where exposed to recover readable code and comments.
- Diff bundles over time to catch newly introduced keys.
# Collect every JS URL the app references, fetch them, then scan
katana -u https://app.example.com -silent -jc \
| grep -E '\.js($|\?)' | sort -u > js_urls.txt
mkdir -p bundles && wget -q -i js_urls.txt -P bundles/
# Hunt high-value key shapes
grep -rEn 'sk_live_[0-9a-zA-Z]{20,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|ghp_[0-9A-Za-z]{36}' bundles/
# Or use a dedicated scanner
trufflehog filesystem bundles/ --only-verified
A leaked key in a compiled bundle looks like this:
const config = {
apiBase: "https://internal-api.example.com",
analyticsKey: "sk_live_9f2c...REDACTED...", // now public
featureFlags: { betaPaymentsAdminPanel: true }
};
Proof of concept
Finding the string is half of it — proving it is live and privileged is the finding. Recover commented source via an exposed source map, then validate the key against the third-party API with a read-only call:
# 1. Pull the source map to recover readable code (note the // sourceMappingURL comment)
curl -s https://app.example.com/static/main.js.map -o main.js.map
npx source-map-explorer bundles/main.js # or unpack the .map to read originals
# 2. Validate the recovered key with a benign, read-only request
curl -s https://api.stripe.com/v1/account \
-u "sk_live_9f2c...REDACTED...:" # 200 with account data == live key
A 200 returning real account data proves the key is valid and active. For a cloud key, the equivalent benign proof is identity, not action:
AWS_ACCESS_KEY_ID=AKIA... AWS_SECRET_ACCESS_KEY=... \
aws sts get-caller-identity # prints the principal == valid creds
Stop at proof — do not exercise destructive or charge-incurring endpoints.
Going further
The severity depends entirely on what the key can do. A read-only, origin-restricted analytics key is minor; a broadly-scoped server-side key inlined by mistake can mean data exfiltration, fraudulent charges, or full account compromise of a third-party service. So after confirming a key is live, enumerate its scope with read-only probes (list resources, read account settings) to show the blast radius.
The build-time variable trap is worth understanding for finding more of these: frameworks that inline NEXT_PUBLIC_/VITE_/REACT_APP_-prefixed variables make it trivial to ship a secret, and the naming convention is a warning, not a protection — grep specifically for those prefixes in bundles. Because the same tooling watches public repos and runs continuously, a leaked key is often found within hours of deploy; assume scanning is already running against everything published.
Capture the bundle URL, the exact key string (redacted in the report), and the validating response proving it is live. Test only keys belonging to assets you are authorized to assess, and use read-only calls.