Sandeep Roy 掃描 978 個 vibe-coded 應用,發現 37 個向訪客洩漏秘密金鑰,並開源 LeakLock
I scanned 978 vibe-coded apps. 1 in 26 ships a secret API key to every visitor.
Sandeep Roy 掃描 978 個公開 vibe-coded 應用,發現 37 個向所有訪客暴露至少一個秘密金鑰,並開源 LeakLock。
A few weeks ago I found an image-generation API key sitting in my own app's JavaScript bundle. Anyone who opened DevTools could copy it and bill me.
The confusing part: Gitleaks said my repo was clean. And it was. The key never touched git. It came in through a VITE_ environment variable I'd set in my hosting dashboard, and the build baked it straight into the bundle.
That's the gap. Secret scanners check your repo. Nobody checks what your site actually serves.
So I scanned what sites serve
I collected 978 public apps built with tools like Lovable, Bolt and v0 (all linked from open GitHub repos) and scanned what each one sends to a browser: the HTML, the JS bundles, lazy-loaded chunks, and source maps.
What came back:
- 1 in 26 apps (37 of 978) shipped at least one secret key to every visitor
- 17 exposed Telegram bot tokens (anyone can take over the bot)
- 9 exposed AI keys: Groq, Gemini, OpenAI, OpenRouter (anyone can run up the bill)
- 6 had critical keys, like GitHub tokens
- 8.8% served public source maps, which hand out the original source code
No sites are named here, and keys were masked during the scan. The point isn't to shame anyone. Every one of these is an honest mistake that the tooling makes very easy.
Why this keeps happening
- Anything prefixed
VITE_,NEXT_PUBLIC_orREACT_APP_goes into the client bundle. That's by design, but it's easy to forget when an AI assistant writesimport.meta.env.VITE_OPENAI_KEYfor you. - The key lives in the host's dashboard, not in git, so repo scanners never see it.
- Many of these keys have no fixed format, so pattern-matching scanners miss them even when they do look.
Catching keys with no known format
Regex works for sk-... or ghp_.... It doesn't work for a random 40-character string that happens to be a key.
So instead of only matching shapes, I look at how the code uses a value. A string sent as Authorization: Bearer, in an x-api-key header, or as ?key= in a request is almost certainly a credential, whatever it looks like. Trace the minified variable back to its literal, read the request host, and you can name the API too.
The opposite problem matters just as much. Supabase anon keys, Firebase web config and Stripe pk_ keys are meant to be public. Flagging them is noise, so they're marked safe.
The tool
I packaged this as LeakLock: open source, MIT, zero dependencies.
npx github:sgroy10/leaklock scan https://your-site.com
Or in CI, scanning your deployed preview on every push:
- uses: sgroy10/leaklock@v1
with:
target: https://your-preview-url.com
fail-on: secret
Repo: https://github.com/sgroy10/Leaklock
If you've shipped a vibe-coded app
Spend two minutes on this:
- Open your live site, then DevTools → Sources, and search the bundles for
key,tokenandsecret. - Anything secret that turns up: rotate it first, then move the call behind a server function or edge function.
- Turn off production source maps unless you actually need them.
What's the worst key you've found in a client bundle, yours or someone else's?
來源:DEV Community · dev.to