The secrets hiding in your JavaScript

Why your "public" key is fine but your secret key is a fire

A scanner finds a key in your JavaScript and you get a jolt of panic. You look closer: it's your Stripe publishable key, the one that's supposed to ship to the browser. False alarm. So you start waving off key warnings, and one day the warning you wave off is the sk_live_ that empties your account.

So which keys in your bundle are public by design, and which are secrets that escaped? That's the only question that matters here, and most key warnings are useless until you can answer it. Some of those strings can sit in the browser forever and cost you nothing. One of them hands a stranger your customer list and your refunds. Telling those two apart is the whole job, and there's one rule that does it cleanly. Get the rule right, because a scanner that cries wolf is worse than no scanner.

The rule: identification versus action

A public key identifies. A secret key acts.

A publishable key like Stripe's pk_live_ tells Stripe which account a request belongs to. On its own it can't move money, read your customers, or do anything you'd mind a stranger doing. It's meant to sit in the browser, the same way your domain name is public. A Supabase anon key is the same idea: it identifies your project, and it's only useful when row-level security is deciding what each request may touch. A Google Maps browser key identifies your billing project and is locked down with an HTTP referrer restriction so it only works on your domain.

A secret key acts on your behalf with full authority. Stripe's sk_live_ issues refunds and reads your customer list. A Supabase service_role key skips every access rule. An AWS AKIA key reaches your buckets. A ghp_ token reads your private source. None of these have a notion of "from my domain only." They work for whoever holds them, from anywhere. That's the line. If the key grants action and has no concept of where it's being used from, it's a secret, and it has no business in the browser.

string found in bundleverdict
pk_live_51HxQp••••YzStripe publishable keysafe
NEXT_PUBLIC_SUPABASE_URLproject URLsafe
AIzaSy••••••••••••••••Google Maps browser keysafe
sk_live_51HxQp••••YzStripe secret keyflagged
eyJ•••service_role•••Supabase service_role JWTflagged
AKIA4PED••••••••EXMPAWS access key idflagged
Same scan, two verdicts. The public keys identify. The flagged ones act, from anywhere, for anyone.

Why this is exactly what we flag, and what we don't

A scanner that flagged every key string would be useless. You'd see pk_live_ lit up red, learn it's fine, and stop reading the report. So the detection has to encode the rule above. Matching on "looks like a key" is what produces the false alarms that train you to ignore the real ones.

We flag the strings that grant action with full authority: sk_live_, service_role JWTs, AKIA access keys, ghp_ and xox and sk- tokens, private keys. We don't flag the strings that are public by design: pk_live_, the Supabase anon key, a referrer-locked Maps key. Same scan, opposite verdicts, because the question isn't "is this a key" but "can a stranger do something with it."

There's an honest limit here. We see the string and we know its type from its shape, so we know a pk_live_ is publishable and an sk_live_ is not. What we can't see from outside is whether a key has been scoped down on the provider's side. An AWS key might be locked to one read-only bucket, or it might own the account, and the key string looks identical either way. So when we flag a secret in your bundle, treat it as live and full-power until you've checked the provider, because that's the safe assumption and the one an attacker makes too.

When a "public" key still bites

Public by design isn't the same as harmless in every case, and this is where the simple rule needs a footnote.

A publishable key with no real restriction can still be abused for volume. A Google Maps browser key with the referrer restriction missing gets lifted and used on someone else's site, running up your maps bill. The key was meant to be public, but "public" assumed a domain lock that wasn't there. You can't hide the key, so the fix is to add the restriction it was always supposed to have.

// Google Cloud key settings
Application restrictions: None
// → works from any site that copies it
A public key is fine when it's scoped to your domain. Unrestricted, it's a free key for whoever copies it.

So the rule holds with one refinement. A secret key in the browser is a fire, full stop, and the fix is to get it out and rotate it. A public key in the browser is fine, as long as it's actually scoped the way "public" assumes. The mistake that costs you money is treating both as the same problem in either direction: panicking over a pk_ you were meant to ship, or shrugging at an sk_ because "all keys leak." The pk_ is just a string. The sk_ is your account, and a stranger with it can spend exactly as freely as you can.

We sort them for you from the outside, the same view an attacker has, and only raise the flag on the keys that can actually be used against you. That's the difference between a report you act on and one you learn to ignore.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.