Exposed .env files, source maps, and API keys in JavaScript: how leaks happen and how to find them.
The three most common ways secrets reach the public web: .env files served by the web server, published source maps, and keys bundled into front-end JavaScript. How to find and fix each.
A leaked secret is any credential, such as an API key, database password, signing secret, or cloud access key, that becomes readable by someone it was not meant for. On the public web, leaks rarely come from sophisticated attacks. They come from a configuration file that ended up in the served directory, a build step that published its debugging output, or a developer who did not realise that a variable with a certain prefix is sent to every browser. This post walks through the three mechanisms we see most, how to detect each on your own site, and what to do when you find one.
How do .env files end up public?
A .env file holds environment variables for local development: database URLs, API keys, secrets. It is meant to be read by the application process and never served. It becomes public when it sits inside a directory the web server exposes, which happens with PHP and static deployments where the project root is the web root, with misconfigured Nginx or Apache rules, and with Docker images that copy the whole project directory into the served path.
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/.env
curl -s https://example.com/.env | head -5A 200 with KEY=value lines in the body is a critical exposure. The same mechanism exposes .git/config and .git/HEAD (which allow the entire repository to be reconstructed), composer.json, package.json with private registry tokens, wp-config.php.bak, and database dumps left in the root. Watch for single-page apps that return their index page with a 200 for every path; the test is whether the body is the file, not the status code.
Fix: keep secrets out of the web root entirely, add an explicit deny rule for dotfiles at the server level, and never copy the project directory into the served directory in a Docker build. Then rotate everything that was in the file.
What do source maps expose?
A source map (.map file) tells a debugger how minified production JavaScript corresponds to the original source. Bundlers generate it with a comment at the end of the bundle: //# sourceMappingURL=main.abc123.js.map. If that file is deployed with the bundle, anyone can download your complete front-end source: file names, comments, internal API routes, feature flags, and whatever configuration was imported into the client.
# find bundles
curl -s https://example.com/ | grep -o -E "src=\"[^\"]+\.js\""
# check the end of one for a map reference
curl -s https://example.com/_next/static/chunks/main-abc123.js | tail -c 150
# does the map actually resolve?
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/_next/static/chunks/main-abc123.js.mapSource maps are not a credential leak on their own; they are a map of your application and a multiplier for every other weakness. Combined with a key in the client, which the next section covers, they are a breach.
Fix: do not publish source maps to the public web. Next.js disables browser source maps in production by default (productionBrowserSourceMaps: false); Vite and webpack need sourcemap: false or a hidden mode that generates the file without the comment. If you need maps for error tracking, upload them to the error tracking service at build time and delete them from the deployment.
Why do API keys end up in the JavaScript bundle?
Modern front-end frameworks let you read environment variables in client code, but only variables with a specific prefix, as a safety mechanism. NEXT_PUBLIC_ in Next.js, VITE_ in Vite, REACT_APP_ in Create React App. Anything with that prefix is inlined into the bundle at build time and shipped to every visitor. The mechanism is well documented and still produces leaks constantly, because a developer who needs a key "on the client" renames the variable to make the error go away, and the key is now public.
| Key type | Safe in client code? | Why |
|---|---|---|
| Public map, analytics, or payment publishable keys | Yes, with domain restrictions | Designed to be public; restrict by referrer or origin in the provider console |
| Payment secret keys (sk_live_...) | No | Full account access; charges and refunds |
| Cloud access keys (AKIA..., service account JSON) | No | Full access to whatever the IAM policy allows, often everything |
| LLM provider keys (sk-..., model API tokens) | No | Unlimited billing on your account; abuse traced to you |
| Database connection strings | No | Direct data access, bypassing the application |
| Internal API URLs and tokens | No | Reveal and often authenticate against private services |
| Feature flag SDK client keys | Usually | Read-only by design, but check the vendor's guidance |
The rule: if the provider calls it secret, private, or server-side, it must stay on the server, behind an endpoint you control that does the call on the client's behalf.
How do you find leaked secrets on your own site?
For the deployed site, pull every JavaScript bundle and search for known key formats and high-entropy strings:
for u in $(curl -s https://example.com/ | grep -o -E "https?://[^\"']+\.js|/[^\"']+\.js" | sort -u); do
case $u in /*) u="https://example.com$u";; esac
curl -s "$u" | grep -o -E "(AKIA[0-9A-Z]{16}|sk_live_[0-9a-zA-Z]{24,}|sk-[A-Za-z0-9]{20,}|AIza[0-9A-Za-z_-]{35}|ghp_[A-Za-z0-9]{36}|xox[baprs]-[0-9A-Za-z-]{10,}|-----BEGIN [A-Z ]*PRIVATE KEY-----)" && echo " ^ in $u"
doneFor the repository, run a secret scanner such as gitleaks or trufflehog across the full history, not just the current tree, because a key removed in a later commit is still in the clone. Enable your code host's push protection so the next one is blocked before it lands.
Pattern matching finds the keys with recognisable prefixes. It misses the ones without: a random 32-character string could be a key or a cache-busting hash. This is where verification matters. Pythorix checks each candidate with a read-only call to the provider where one exists, so the report says "live AWS key" rather than "string that looks like a key", and it does this across every bundle on every subdomain, on every scan. The false-positive discipline is described in Scoring real breach risk.
What should you do when you find a leaked secret?
- Rotate it first. Generate a new credential and revoke the old one at the provider. Do this before removing it from the code; the leak is already public, and removal does not un-leak it.
- Remove it from the source and from the deployment. For git history, rewriting is often more trouble than it is worth once the key is rotated; the old key is dead.
- Check the logs. Look for use of the old credential from unfamiliar IP addresses or in unusual volumes. For cloud keys, check for resources you did not create. For LLM keys, check the billing dashboard.
- Decide whether it is a breach. If the key gave access to personal data and there is evidence of use, you may have notification obligations under GDPR, DPDP, or contract. See the minimum scan before an audit for the compliance context.
- Fix the mechanism, not just the instance: deny dotfiles at the server, stop publishing maps, move the call server-side, add push protection.
Frequently asked questions
Is it safe to put an API key in NEXT_PUBLIC_ or VITE_ variables?
Only if the provider designed that key to be public, such as a publishable payment key or a map tile key, and you have restricted it by domain in the provider console. Any key described as secret, private, or server-side must never carry a public prefix.
How do I check if my website exposes its .env file?
Request https://yourdomain/.env with curl and look at the body, not just the status code. If it returns lines in KEY=value form, the file is public. Also check .git/HEAD, .git/config, and common backup names.
Are source maps a security risk?
They expose your complete front-end source, including internal routes, comments, and configuration. That is not a credential leak by itself, but it maps your application for an attacker and often reveals keys or endpoints that would otherwise be hidden in minified code. Do not publish them in production.
What should I do if an API key was leaked?
Rotate it immediately at the provider, then remove it from code and deployments, then review usage logs for the old key. If it protected personal data and shows signs of use, assess your breach notification obligations.
Can a scanner tell whether a leaked key is live?
A good one can. Pythorix verifies candidate keys with read-only provider calls where available, so the finding states whether the credential is active rather than reporting every key-shaped string.