Your API key is in your JavaScript bundle
A leaked .env is a server file you forgot to hide. This is worse in one specific way: the key isn't sitting in a file waiting to be found — you shipped it, on purpose, to every browser that loads your app.
Here's the failure in one sentence: you needed to call Stripe (or OpenAI, or your database) from your app, so you pasted the key into your frontend code, and it worked. It kept working. What you didn't see is that your build bundled that key into the JavaScript it serves to the public. Anyone who opens devtools, hits View Source, or reads the network tab can copy it in about three seconds.
This is the single most common way an AI-assisted build springs a leak. The assistant writes code that calls an API, the key has to go somewhere, and “in the component” is the path of least resistance. It compiles, the demo works, and a live secret is now public. Scanners crawl for exactly these patterns — a sk_live_ string in a .js file is a screenshot waiting to happen, and it's not one you can shrug off as shakedown padding.
Publishable key vs. secret key
Not every key in your frontend is a disaster. The whole game is knowing which kind you shipped. Providers hand you two flavors, and they are not interchangeable:
Publishable / restricted keys — meant for the browser
Stripe's pk_live_, a Supabase anon key, a Google Maps browser key. These are designed to be public — their power is fenced in by the provider (Stripe can only start a checkout, the Supabase anon key is gated by row-level security). Shipping these is normal. The risk is only real when the fence behind them is missing.
Secret / service keys — never leave the server
Stripe's sk_live_, an OpenAI sk- key, an AWS AKIA… access key, a Supabase service_role key, a GitHub ghp_… token. These carry your full authority. In a browser they mean a drained API bill, a wiped database, or worse. If one of these is in your bundle, it's an incident.
The NEXT_PUBLIC_ footgun
Modern frameworks make this trap almost frictionless. Any env var starting with NEXT_PUBLIC_ (Next.js) or VITE_ (Vite) is deliberately inlined into the client bundle — that's the whole point of the prefix. So the moment someone renames STRIPE_SECRET_KEY to NEXT_PUBLIC_STRIPE_SECRET_KEY to “fix” an undefined error, they've quietly published it. The variable was in a .env file the whole time — the prefix is what pushed it over the wall.
Rule of thumb: the NEXT_PUBLIC_ / VITE_ prefix means “print this on a billboard.” If you would not put the value on a billboard, it does not get that prefix.
Check it in one minute
You don't need a scanner to catch the obvious cases. Load your live site, open devtools, and search the loaded scripts for the tell-tale prefixes:
If a real secret comes back, treat it as a live incident, not a to-do — the same way you would an exposed .env. Keep reading.
What to actually do
Order matters. Rotate first, because the moment a secret is in a public bundle you have to assume it's already copied.
- 1
Rotate the leaked key. Assume compromised.
Roll the secret in the provider's dashboard and revoke the old one. Stripe, OpenAI, AWS, GitHub, Supabase all do this in a couple of clicks. This is the step people skip, and it's the only one that actually stops the bleeding — pulling the key from your next deploy does nothing for the copy someone already grabbed. - 2
Move the call to the server.
The secret should only ever be used somewhere the browser can't see: a route handler, an API route, a serverless function, an edge function. Your frontend calls your endpoint; your endpoint holds the key and talks to Stripe or OpenAI. The key never crosses the wire to the client. - 3
Drop the NEXT_PUBLIC_ / VITE_ prefix.
A secret read on the server needs a plainSTRIPE_SECRET_KEY, notNEXT_PUBLIC_…. If removing the prefix throws an “undefined” error, that error is correct — it means you were reading the secret in client code, which is exactly the bug. Move the read to the server instead. - 4
Add a rate limit or usage cap while you're there.
Even publishable keys get abused. Put a spend cap on the provider, or a rate limit on your own endpoint, so a scraped or brute-forced key can't run up an unbounded bill before you notice.
Why the browser can never keep a secret
There's no obfuscation, minification, or “environment variable” trick that fixes this, because the browser has to be able to read the code in order to run it. Anything your JavaScript can see, your visitor can see. That's not a bug in the framework — it's the nature of shipping code to a machine you don't control. The only real fix is to keep the secret on a machine you do control and put an endpoint in front of it.
Do that, and the scariest string on your report — a live key anyone could copy — turns into a boring server call nobody can see. Nothing to screenshot, nothing to sell you.
Want to know if a live Stripe, OpenAI, or AWS key is sitting in your bundle right now? We read your scripts and flag the ones that matter. Scan in about 20 seconds.
Scan your site free →