Website problems
Secrets in public files
A secret in a public file is a credential — an API key, a token, a password, a private key — reachable by anyone who requests the URL. It happens most often in front-end JavaScript, where a developer adds a key to make something work and the bundle is served to every visitor. The critical distinction is between keys that are public by design, which are safe there, and keys that authorise action, which are not. Once a secret has been served, rotating it is the only remedy that works.
Public by design against genuinely secret
Some keys belong in front-end code. A Stripe publishable key, a Google Maps browser key, a Supabase anon key, a public analytics identifier: each is designed to be visible and is constrained by server-side rules about what it can do.
Others authorise action on your behalf. A Stripe secret key, a database connection string, a cloud access key, an SMTP password, a webhook signing secret, a service-account private key. None of these belong anywhere a browser can reach.
The naming usually tells you — `pk_` against `sk_`, `publishable` against `secret`, `anon` against `service_role` — and the naming is not reliable enough to depend on alone.
The test that is reliable: if the key can do something the visitor holding it should not be able to do, it is secret. A publishable key that can create a charge is a secret key wearing the wrong prefix.
A public key still needs its restrictions configured. A Maps browser key with no referrer restriction is public by design and abusable by anyone.
Where they end up
Bundled JavaScript, when a build tool inlines an environment variable that was not meant for the client. Any variable exposed to a front-end build is published — the prefix conventions exist precisely to make that visible.
Configuration files left in the web root: `.env`, `config.json`, `settings.py`, a backup of a deployment file.
Source maps, which carry the original source including whatever was in it.
Version-control directories served by mistake, where the repository history contains every key ever committed, including the ones later removed from the working tree.
Backup and editor artefacts: `.bak`, `.old`, `.swp`, a copy of a file made during a fix and never deleted.
Public repositories, which are outside the site and are where automated scanners look first.
Why rotation is the only remedy
Removing the file stops future requests. It does nothing about the requests already made.
Automated scanners fetch common paths continuously and index what they find. A key exposed for an hour should be assumed collected.
Repository history is worse: deleting a key in a later commit leaves it in every clone and in the history anyone can read.
So the sequence is: rotate first, then remove the file, then work out how it got there. Rotating last means the exposure continues while you investigate.
After rotation, check the service's own audit log for use of the old credential. That is the only evidence available about whether anyone took it.
How to check it yourself
Request the obvious paths: `/.env`, `/config.json`, `/.git/config`, `/backup.sql`. Each should return 404, and a 200 on any of them is an incident rather than a finding.
Search the production bundle for key-shaped strings: long base64 or hex runs, and the known prefixes of the services you use.
Check what your build exposes. Every framework has a convention for which environment variables reach the client, and anything matching it is published by definition.
Search the repository history rather than the working tree, since a removed key remains in the history.
Run a secret scanner in the build. This is the only part that keeps working after the review is over.
How to prevent it structurally
Keep secrets in a secret manager, injected at runtime rather than at build time, so a front-end build has nothing to inline.
Use the naming convention deliberately: if a variable does not carry the public prefix, the build should refuse to inline it.
Block secrets at commit time with a pre-commit scanner, and again in the pipeline. The commit hook catches the honest mistake; the pipeline catches the bypassed hook.
Deny access to dotfiles and version-control directories at the web-server level, once, for everything.
Scope every public key to the origins that may use it, so a key that is meant to be visible is still not usable by anyone else.
How VeriFixScan detects it
`exposure.secrets.private` examines the publicly reachable assets of the crawled pages for credential patterns that should not be public, and reports what it finds with the file it appeared in.
`exposure.secrets.public_keys` reports the keys that are public by design, as an inventory rather than as a fault — the useful question there is whether each one is restricted.
`exposure.internal.paths` and `exposure.comments.sensitive` cover the adjacent disclosures: server paths and revealing comments left in public assets.
The checks read files that are publicly served. They do not attempt authentication, do not guess credentials, and do not use anything they find.
Frequently asked questions
- Is a publishable API key in front-end code a problem?
- Not if it is genuinely public by design and correctly restricted. A publishable Stripe key or a Maps browser key belongs there; a secret key or a service-role key does not.
- I removed the file. Is that enough?
- No. Assume anything publicly served has been collected, and rotate the credential. Removal stops future exposure and does nothing about past requests.
- How do secrets end up in a front-end bundle?
- Usually an environment variable exposed to the client build. Frameworks publish every variable matching their public prefix, so a secret named with that prefix is inlined into the bundle by design.
Sources
Related
VeriFixScan crawls a site and applies its checks to every page it reaches, keeping the evidence behind each finding. Scanning one website is free.
Scan a website