How to Find and Fix API Key Leaks in Your SaaS App
In 2024, a developer exposed a Google Gemini API key in a public GitHub repo and got charged $250,000 in a single weekend. Leaked Stripe secret keys have drained accounts overnight. Here's how to find and fix these leaks before they cost you.
On this page▾
1How API Keys End Up in JavaScript Bundles
When you build a Next.js, React, or Vite app, your bundler (webpack, Vite, Rollup) compiles all your imports into JavaScript bundles. Any environment variable that starts with NEXT_PUBLIC_ or is referenced directly in client-side code gets included in those bundles — readable by anyone who opens DevTools.
The common mistake with AI-assisted coding
You ask Claude or ChatGPT to “add Stripe integration” and the AI generates code that uses process.env.STRIPE_SECRET_KEY in a client component. Your bundler bakes the key directly into _next/static/chunks/app.js. Everyone can read it.
2The 17 Keys We Scan For
Our security scanner checks HTML source and all JavaScript bundle files for these patterns:
Free 30-second key leak scan
Fetches all your JS bundles and matches against 17 secret patterns.
3How to Find Leaks Right Now
Option 1: Use our free scanner. Paste your URL at aiexposuretool.com/security. We fetch all your JS bundles and scan for all 17 patterns automatically. Results in 15 seconds.
Option 2: Manual check. Open your site in Chrome, open DevTools → Sources → look for files in _next/static/chunks/. Use Ctrl+F to search for sk_live, sk-, service_role.
4How to Fix API Key Leaks
- Rotate the key immediately. Go to your provider (Stripe, OpenAI, etc.) and generate a new key. Revoke the old one. Do this before anything else — assume the key has already been compromised.
- Move the key to server-side only. In Next.js, any variable used in a Route Handler, Server Component, or Server Action is server-side only. Never prefix secret keys with
NEXT_PUBLIC_. - Create a server-side API wrapper. If you need to call an external API from the frontend, create a Next.js Route Handler (
/api/your-endpoint) that makes the external call server-side and returns only what the client needs. - Audit your .env files. Make sure
.env.localand.env.productionare in your.gitignore. Run a search in your repo forsk_liveor your actual key values.
5Prevent Leaks Going Forward
- Use a secret scanning tool in CI (GitHub has native secret scanning for public repos)
- Never use
NEXT_PUBLIC_prefix for any key that isn't safe for public consumption - Run our security scanner before every major release
- Set up .env file exposure alerts — we check if your .env is publicly accessible
Pre-launch security checklist
The 5-minute audit every indie hacker should run — covers keys, headers, .env, source maps, and more.
Frequently Asked Questions
How do API keys end up in JavaScript bundles?
When a bundler (webpack, Vite, Rollup) compiles your imports, any environment variable prefixed NEXT_PUBLIC_ — or any value referenced directly in a client component — gets baked into the public JavaScript. Anyone with DevTools can read it. AI coding tools commonly make this mistake when generating Stripe / OpenAI / Supabase integration code.
What kinds of keys does the scanner detect?
17 patterns including: Stripe secret keys (sk_live_), Stripe webhook secrets (whsec_), OpenAI keys (sk-), Anthropic / Claude keys, Google Gemini keys, Supabase service role keys, Razorpay, AWS access keys, Twilio auth tokens, SendGrid, GitHub PATs, Slack bot tokens, Mailgun, HubSpot, Cloudflare API tokens, Intercom, Braintree.
What should I do if I find a leak?
Rotate the key IMMEDIATELY in the provider's dashboard. Don't try to remove the key from your repo first — assume it's already been scraped. Then move the key to server-side only (Route Handler, Server Component, or Server Action), refactor any client-side calls into API routes, and redeploy.
How do I check manually without a scanner?
Open your site in Chrome, open DevTools → Sources, look for files in _next/static/chunks/, then Ctrl+F search for 'sk_live', 'sk-', or 'service_role'. The scanner automates this across 17 patterns and your full bundle in 15 seconds, but a manual check works for a quick spot-test.
How can I prevent leaks going forward?
Five practices: (1) enable secret scanning in CI (GitHub has native scanning for public repos), (2) never use NEXT_PUBLIC_ for any sensitive value, (3) run a security scan before every release, (4) keep .env.local / .env.production in .gitignore, (5) set up .env exposure alerts — most scanners check if /.env is publicly accessible at the URL.