WHERE VIBE CODERS LEAK THEIR KEYS: 2026 FRONTEND SECRETS REPORT
Forty-one percent of AI-built apps in our 2026 corpus ship at least one secret key in their frontend bundle. Stripe, Supabase service-role, OpenAI, AWS — what is leaking, where, and on which platforms.
We loaded 1,514 AI-built apps as a normal browser would and ran 100+ key signatures against everything the browser fetched. Six hundred and fourteen of them — 41% — shipped at least one secret key. This is the breakdown by key type, platform, and modal location.
Headline numbers
| Metric | Value |
|---|---|
| Apps scanned | 1,514 |
| Apps with at least one leaked secret | 614 |
| Leak rate | 41% |
| Median leaked keys per affected app | 1 |
| Apps leaking 3 or more keys | 87 |
| Window | Nov 2025 – Apr 2026 |
What is leaking, by key type
| Rank | Key type | Apps affected | Share of leaking apps | Severity floor |
|---|---|---|---|---|
| 1 | Stripe secret key (sk_live_, sk_test_) |
134 | 22% | Critical (full charge/refund) |
| 2 | Supabase service-role JWT | 119 | 19% | Critical (RLS bypass) |
| 3 | OpenAI API key (sk-proj-, sk-) |
102 | 17% | High (billing abuse) |
| 4 | AWS access key (AKIA*, ASIA*) |
78 | 13% | Critical (depends on IAM) |
| 5 | Google / Firebase service key (AIza*) |
71 | 12% | High (depends on rules) |
| 6 | Anthropic API key (sk-ant-) |
58 | 9% | High (billing abuse) |
| 7 | SendGrid / Mailgun / Resend keys | 41 | 7% | High (email abuse) |
| 8 | Twilio auth tokens | 28 | 5% | High (SMS billing) |
| 9 | GitHub personal access tokens | 22 | 4% | Critical (repo access) |
| 10 | Generic high-entropy strings (likely secrets) | 117 | 19% | Triage required |
Stripe secret keys are the headline — 134 apps shipping a sk_live_ or sk_test_ to every visitor. The publishable / secret distinction is exactly where vibe-coding fails: builders paste both into the AI prompt because both are labeled “key”, and the AI does not consistently route the secret through a backend.
Where the keys are hiding
| Location in the bundle | Share of findings |
|---|---|
| Inlined in JavaScript bundle (Webpack/Vite/esbuild output) | 64% |
Inlined in HTML head as window.__ENV or similar |
18% |
| Returned by an unauthenticated API call | 9% |
Inlined in .env.js or config.js shipped to client |
6% |
| Visible in source maps that shipped to production | 3% |
The 3% in source maps is the easiest fix and the most embarrassing — production builds with source maps publish your full source tree to the public. Most build tools default to producing source maps; few default to excluding them from production deployment.
Per-platform leak rate
| Platform | Apps in sample | Leak rate | Modal leaked key |
|---|---|---|---|
| Lovable | 612 | 47% | Supabase service-role JWT |
| Bolt.new | 318 | 51% | Stripe secret key |
| Cursor | 246 | 32% | OpenAI API key |
| Replit | 201 | 38% | AWS access key |
| V0 | 137 | 24% | Stripe secret key |
Bolt.new’s higher rate reflects the platform’s preference for frontend-only quick-prototype patterns. Cursor’s lower rate is partly an artifact — Cursor users are more likely to be technical, more likely to introduce a backend, and therefore less likely to ship a secret to the browser at all. Lovable’s modal leak is service-role keys because builders paste them into the prompt as “the key” without realizing Lovable’s workflow expects only the anon key.
CWE / OWASP mapping by leak shape
The CWE varies with how the key reached the browser. The fix shape varies the same way.
| Leak shape | CWE | OWASP | Fix shape |
|---|---|---|---|
| Inlined in JS bundle (build-time substitution) | CWE-798 Hard-coded Credentials | A02:2021 Cryptographic Failures · A05:2021 | Move to server-only env; route the integration through a backend handler |
Inlined via window.__ENV or <script> block |
CWE-200 Sensitive Info Exposure | A05:2021 Security Misconfiguration | Same as above; never inject secrets into the SSR’d HTML |
| Returned by an unauthenticated API call | CWE-522 Insufficiently Protected Credentials | A07:2021 Identification and Auth Failures | Gate the endpoint behind auth; return only what the client genuinely needs |
Inlined in .env.js / config.js shipped to client |
CWE-540 Inclusion of Sensitive Info in Source Code | A05:2021 Security Misconfiguration | Server-only config; do not ship config files to the browser |
| Visible in source maps shipped to production | CWE-538 Insertion of Sensitive Info into Externally-Accessible File | A05:2021 Security Misconfiguration | Disable source maps for production builds, or upload-only-to-Sentry |
| LLM-prompt-embedded secret reflected in output | CWE-200 / LLM07:2025 | OWASP LLM Top 10 — System Prompt Leakage | Never include live secrets in prompts; use signed short-lived tokens |
The LLM-prompt path is the newest leak shape and the one most under-tested. We see it when builders paste sk_live_ into a Lovable prompt as “the Stripe key”, the model embeds it in a code path that reflects the prompt content into a debug error — and the error surfaces in production.
Per-leak-shape fix patterns
The mechanical fix per shape is short. Most teams stop at rotation and skip the proxy step — that is what produces re-leaks two weeks later.
# Step 1, every shape: rotate the key in the provider dashboard.
# Stripe: sk_live_ → revoke, generate new restricted key
# Supabase: project settings → reset service-role JWT
# OpenAI: platform.openai.com/api-keys → revoke + regenerate
// Step 2 for "inlined in JS bundle": move the secret to a server-only env,
// route the integration through a backend handler.
// WRONG — Vite inlines anything prefixed VITE_ into the bundle.
const stripe = new Stripe(import.meta.env.VITE_STRIPE_SECRET_KEY);
// RIGHT — secret stays server-only, frontend hits your route.
// app/api/charge/route.ts (Next) or supabase/functions/charge (Edge Function)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const session = await getSession(req);
if (!session) return new Response(null, { status: 401 });
// ... charge logic, returning only the public confirmation to the client.
}
// Step 2 for "returned by unauthenticated API call": auth-gate, then minimize.
// WRONG — public endpoint reflects the whole config object.
app.get('/api/config', (req, res) => res.json(serverConfig));
// RIGHT — return only what the client genuinely needs, post-auth.
app.get('/api/config', requireAuth, (req, res) => {
res.json({ stripe_publishable_key: serverConfig.STRIPE_PUBLISHABLE_KEY });
});
// Step 2 for "source maps in production": ship maps off-platform.
// vite.config.ts
export default defineConfig({
build: {
sourcemap: 'hidden', // generate but do not reference from production assets
},
});
// Upload the maps to Sentry / Datadog at deploy time; never to the public bucket.
For LLM-prompt leaks, the fix is a hard rule: secrets never enter the prompt context. If the model needs to act on behalf of a user, mint a short-lived signed token scoped to the action and pass that — not the upstream API key.
The 24-hour cost of a Stripe sk_live_ leak
For the 134 apps leaking a Stripe secret key, we modeled the financial exposure assuming the key would be discovered within the industry-standard 5-15 minute window:
- Median apps’ Stripe daily volume. $440 (estimated from public payment-form metadata)
- Median refundable balance accessible. Last 30 days of charges, ~$13,200
- Median attacker-controllable refund window. 90 days from charge, full balance
A sk_live_ key in a frontend bundle gives an attacker the ability to refund every charge to a card they control. The financial damage of a single key leak ranges from “a Sunday’s revenue” to “the last quarter’s revenue”, depending on Stripe’s velocity flags.
Methodology
Sample. All 1,514 apps scanned between Nov 2025 and Apr 2026. The Token Leak Checker probe was applied to each.
Probe. Each app was loaded as a normal browser. Every fetched resource — initial HTML, JavaScript bundles, dynamic imports, source maps if served — was parsed and matched against 100+ regular expressions for known key formats, plus a high-entropy heuristic for likely-secret strings of unknown format.
Validation. Stripe and Supabase keys were validated structurally (format and checksum where present); we did not attempt any authenticated request against the provider, so the “leak rate” measures keys that look valid by format and were reachable, not keys we confirmed are active.
De-duplication. Multiple occurrences of the same key in the same bundle count once. Multiple distinct keys of the same type in the same app count separately.
Limits. This study measures keys present in the static or runtime-loaded bundle visible to a normal browser. It does not measure keys exposed only after authentication, keys exposed via debug endpoints triggered by query parameters, or keys exposed by side channels (server logs, error responses on edge cases).
Calibration against ref0. Every probe is also run against ref0 — a clean reference site that ships zero secrets. Any signature that fires on ref0 is a false positive and the rule is killed. The 100+ signatures in production are net of false-positive elimination; the per-key counts in the table above reflect only signatures that distinguished ref0 from a deliberately leaky scenario.
Reproduce on the public benchmark
Each leak shape maps to a live scenario on gapbench.vibe-eval.com. The scenarios are running; the curl commands return real, deliberate-by-design leaks today.
| Scenario | URL | What leaks |
|---|---|---|
| Indie SaaS | /site/indie-saas/ | Stripe sk_live_ inlined in bundle |
| Supabase clone | /site/supabase-clone/ | Supabase service-role JWT in window.__ENV |
| Agent app | /site/agent-app/ | OpenAI / Anthropic API keys via VITE_* env |
| Sentry DSN leak | /site/sentry-dsn-leak/ | Sentry DSN with abusable scope |
| Config leak | /site/config-leak/ | Centralized config.js shipped to client |
| Source-map leak | /site/source-maps-and-git-exposed/ | Production .js.map and .git/ exposed |
| ref0 (clean control) | /site/ref0/ | Nothing — the false-positive reference |
For the anatomy of how Supabase service-role keys end up in frontend bundles and how the fix looks per generator, see the companion pattern walkthrough: The Supabase service-role key in your frontend bundle and Source maps and .git in production.
How to reproduce a single data point
- Open the Token Leak Checker.
- Paste your live URL.
- Read the report. Any key marked critical or high must be rotated immediately, then proxied behind a backend before re-deploy.
Citations
VibeEval. Where Vibe Coders Leak Their Keys: 2026 Frontend Secrets Report. May 2026. https://vibe-eval.com/data-studies/frontend-secrets-leak-report-2026/
Related
- Pattern walkthrough: The Supabase service-role key in your frontend bundle
- Pattern walkthrough: Source maps and .git in production — Next.js leaks you didn’t know you shipped
- Pattern walkthrough: Stripe trust on the wrong side — webhook signatures skipped, paid-flag tampering
- Data study: 2026 AI App Security Benchmark
- Data study: Supabase RLS in the Wild — 2026 Misconfiguration Atlas
- Data study: Honeypot Supabase: How Long Does a Public Anon Key Survive?
- Tool: Free Token Leak Checker
RUN IT YOURSELF
Each scenario below is live on the public benchmark. The commands are copy-paste ready. Outputs may evolve as we tune the scenarios; the bug stays.
curl -s https://gapbench.vibe-eval.com/site/indie-saas/ | grep -oE 'sk_(live|test)_[A-Za-z0-9]{20,}'
curl -s https://gapbench.vibe-eval.com/site/supabase-clone/ | grep -oE 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' | head -1
curl -s https://gapbench.vibe-eval.com/site/agent-app/ | grep -oE 'sk-(proj-)?[A-Za-z0-9_-]{40,}'
curl -s https://gapbench.vibe-eval.com/site/sentry-dsn-leak/ | grep -oE 'https://[a-f0-9]+@[a-z0-9.-]+/[0-9]+'
curl -s https://gapbench.vibe-eval.com/site/ref0/ | grep -oE 'sk_(live|test)_[A-Za-z0-9]{20,}|eyJ[A-Za-z0-9_-]+'
COMMON QUESTIONS
SCAN YOUR BUNDLE FOR LEAKED KEYS
The free Token Leak Checker runs 100+ key signatures against your live JavaScript bundle in 15 seconds.