HomeBlog › Exposed secrets

Exposed secrets: how API keys end up in public code

Nobody pastes an API key into a public file deliberately. It arrives through a build step, a commit made at speed, or a config file that was never meant to ship.

Exposed secrets illustration: a laptop showing a public code repository with a live API key highlighted on one line of a config file, and a scan panel reporting three secrets found, the API key rated high risk.

Secrets in public code are rarely a discipline failure. They are a plumbing failure. A build tool inlines an environment variable it should have kept on the server. A config file gets committed before anyone adds it to .gitignore. A deployment copies the whole working directory, including the .git folder.

The result is the same in every case: a credential that should have stayed private is now readable by anyone who looks, and increasingly by automation that does not need to look very hard.

The three routes that account for most of it

1. Bundled into the frontend

Modern build tools substitute environment variables into client-side code at build time. That is the intended behaviour - it is how a public API endpoint or a feature flag reaches the browser. The trouble comes when a variable holding a private key follows the same path.

Frameworks try to prevent this with naming conventions: only variables prefixed NEXT_PUBLIC_, VITE_, REACT_APP_ and so on get inlined. The convention works right up until somebody adds the prefix to make a build error go away.

Everything in a client-side bundle is readable by every visitor. There is no minification, obfuscation or environment-variable indirection that changes this - the browser must be able to read the value in order to use it, and so can anybody else.

2. Committed to a repository

A .env file, a service-account JSON, a key pasted into a config while debugging and forgotten. If the repository is public, that value is public. If the repository later becomes public, every value in its history becomes public at that moment, including ones deleted long ago.

3. Deployed to a web-reachable path

The quiet one. A deployment process copies a directory wholesale and the web server serves whatever it finds:

  • /.git/ - a deployed Git directory allows the entire repository, including full history, to be reconstructed by anyone.
  • /.env - served as plain text unless explicitly blocked.
  • /config.json, /backup.sql, /.aws/credentials - anything that was in the folder at deploy time.

These are checked automatically by anyone scanning at scale, because the paths are predictable and the payoff is high.

Why "nobody will find it" is not a plan

Automated discovery is the norm now, not the exception. Public code hosting is continuously scanned - by the platforms themselves, by defensive services offering leak detection, and by people looking for credentials to use. Several cloud providers run their own scanning and will proactively revoke a key they find published.

The practical consequence: the window between publishing a secret and it being found is not something you can rely on. Assume any credential that has been publicly readable, for any length of time, is compromised.

Finding what you are currently exposing

Start with what a stranger can reach, because that is the most urgent category.

# sensitive paths - 200 is bad, 403 or 404 is what you want
for p in /.git/HEAD /.env /config.json /backup.sql; do
  printf '%-18s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' "https://yourdomain.com$p")"
done

# key-shaped strings in your own bundle
curl -s https://yourdomain.com | grep -oE '(AKIA[0-9A-Z]{16}|sk_live_[0-9a-zA-Z]{24,}|AIza[0-9A-Za-z_-]{35}|ghp_[0-9A-Za-z]{36})'

Then your repository history, which needs a dedicated tool - gitleaks and trufflehog are the common ones - because manual inspection of history does not scale and misses the file deleted three years ago.

One caveat on pattern matching: it finds credentials with recognisable formats. AWS access keys, Stripe live keys and GitHub tokens all have distinctive prefixes. A database password, an internal HMAC secret or a bare hex string does not, and no scanner will spot it. Pattern-based detection is a floor, not a guarantee.

Which keys may be public, and which may not

Not every credential is a secret. Some are designed to sit in a browser, and confusing the two categories in either direction causes problems - either a real secret gets shipped, or an engineer spends a day trying to hide a value that was always meant to be visible.

Safe in the browserNever in the browser
Publishable / public API keysSecret or private API keys
Anonymous keys protected by server-side access rulesService-role or admin keys that bypass those rules
Public site keys for bot-protection widgetsThe matching server-side verification secret
Analytics measurement IDsDatabase connection strings

The distinction is what the key can do when held by a stranger. A publishable key that can only perform operations you would allow any visitor to perform is fine in a bundle. A key whose entire purpose is to bypass authorisation is not, regardless of how well it is hidden.

The dangerous case is a pair that looks like one thing: platforms commonly issue an anonymous key and a service-role key that are similar in shape and sit adjacent in the dashboard. Copying the wrong one into a frontend environment variable is a single-character mistake with an unlimited blast radius.

Stopping it before the commit

Detection after publication is damage control. The cheaper intervention is earlier in the pipeline.

  • A pre-commit hook running a secret scanner over staged changes catches the value before it ever enters history. gitleaks protect --staged is a common one-liner for this.
  • CI scanning as a second net, because hooks live on developer machines and a machine without them will eventually push.
  • Provider-side push protection, offered by the major code hosts, which rejects pushes containing recognisable credentials.
  • A committed .env.example holding key names with empty values, so nobody is tempted to commit the real file for reference.

None of these catch a secret with no recognisable format. They do catch the large majority of real incidents, which involve credentials from major providers with distinctive prefixes.

When one has leaked

The order matters, and the first step is the one people skip in the rush to hide the evidence.

  1. Rotate the credential. Immediately, before cleanup. Until the old value is revoked, nothing else you do reduces the risk.
  2. Check for use. Review access logs and billing for activity you did not authorise. Cloud provider costs are often the first visible symptom of a stolen key.
  3. Clean up the exposure. Remove the file, purge it from history, block the path at the web server.
  4. Fix the pipe. A leak that happened once through a build process will happen again unless the process changes. Add the pattern to .gitignore, add secret scanning to CI, and move the value into a secrets manager.

Rewriting history feels like the fix because it makes the evidence disappear. It does not help. Clones, forks, CI logs, caches and anyone who already fetched the value are all unaffected by a force push.

Keeping them out

  • Secrets belong in a secrets manager, injected at runtime - not in files, not in the repository, not in the build environment for client code.
  • Block sensitive paths at the web server so a deployment mistake does not become a public one.
  • Run secret scanning in CI so the check happens before the merge rather than after the leak.
  • Prefer short-lived credentials. A token valid for an hour is a far smaller problem than a static key valid until someone revokes it.
  • Treat public keys as public. If a value must reach the browser, use a key type designed for that, scoped so that possessing it grants nothing dangerous.
What a scan can and can't confirm here. An external check reads your public homepage and probes a handful of well-known sensitive paths. That covers the highest-frequency mistakes, and a finding is a real one. But a clean result is narrow: it means no recognisable key patterns appeared on the page that was checked, and those specific paths were not served. It says nothing about your other pages, your repository history, your CI logs, or any secret whose format is not distinctive enough to match. Use it as a smoke alarm, not an audit.

Where this sits

Exposed secrets are the one item on an external report where a single finding can be genuinely serious on its own. A missing header is a missing layer; a live AWS key in a public bundle is an open door.

That asymmetry is worth keeping in mind when reading any report, including ours. Most findings are about hygiene and accumulate into a picture. This one occasionally is not - it is a specific credential, in a specific file, that needs rotating today. The rest of the surface is covered in external security posture.

Questions people ask about this

How quickly are leaked API keys found?

Fast enough that you should assume automated discovery rather than hope for obscurity. Public code hosting is continuously scanned by both defensive services and opportunistic actors, and newly published secrets are routinely picked up by automation rather than by a person browsing. Treat any key that has been public as compromised.

Is it safe to put an API key in frontend JavaScript?

Only if the key is designed to be public - a publishable key scoped to operations that are safe for anyone to perform, protected server-side by rules. Anything else, including any key with write or admin scope, must never reach the browser. Everything in a client-side bundle is readable by every visitor.

What should I do if a secret has been exposed?

Rotate it first, before anything else. Deleting the file or the commit does not help, because the value may already be copied, cached or indexed. Once the old credential is revoked, then clean up history, check access logs for use you did not authorise, and fix the process that allowed it to ship.

Can removing a commit from Git undo a leak?

No. Rewriting history removes the value from the repository, but not from clones, forks, caches, CI logs, or anyone who already retrieved it. History rewriting is a cleanup step after rotation, never a substitute for it.

Check what your public pages expose

A passive scan reads your homepage HTML and scripts for key-shaped strings and probes for sensitive paths.

Run a free check