AI, payment or database key in the code
A secret key for an AI, payment or database service is sitting in your current code, readable by anyone who can read the repo.
- Severity
- High (depends on the key)
- Who can exploit it
- Anyone who can read the repo, or your site if it ships to the browser
- Found by
- PatchLabs Code: key scan of the current code
- Lab proof
- Re-scan; the real fix is rotation
What it is
Some keys are meant to be public: the Supabase anon or publishable key, Stripe's pk_live_… publishable key, a Firebase web config. Others are secrets that act as your account: an OpenAI (sk-proj-…) or Anthropic (sk-ant-…) API key, a Stripe secret key (sk_live_…) or restricted key (rk_live_…), a database connection string with its password in it. PL-009 means one of those secrets is in the current code of your repo, in a source file or a committed .env.
Anyone who can read the repo can use it: run AI requests on your bill, read or refund payments, or connect straight to your database. If the key is also used in frontend code, it ships in your JavaScript bundle and every visitor to your site has it too, public repo or not.
Two neighbours: a Supabase service role or secret key is PL-010, because it bypasses every access rule; a key that has been removed from the code but is still in old commits is PL-011.
Why AI builders produce it
"Add an AI chat" or "take payments" is one prompt, and the quickest working version puts the key where the code runs. In a Vite or React app that is the browser, often through a VITE_ variable, which the build copies into the bundle on purpose. A committed .env file does the rest when it isn't in .gitignore.
Example of vulnerable code
Simplified from the patterns we see in AI-built apps. Synthetic: not taken from any real app.
// src/lib/ai.ts
import OpenAI from 'openai';
export const openai = new OpenAI({
apiKey: 'sk-proj-••••••••••••', // a real key here works for anyone
dangerouslyAllowBrowser: true,
});
# .env (committed)
STRIPE_SECRET_KEY=sk_live_••••••••••••
DATABASE_URL=postgresql://postgres:••••••@db.xyz.supabase.co:5432/postgres
Check it yourself
# 1. Secret-shaped strings in the current code
grep -rnE "sk-proj-|sk-ant-|sk_live_|rk_live_|AIza[0-9A-Za-z_-]{20}|postgres(ql)?://[^:@/]+:[^@/]+@" \
--exclude-dir=node_modules --exclude-dir=.git .
# 2. Is an env file tracked by git?
git ls-files | grep -E "(^|/)\.env($|\.)" | grep -v example
# 3. Did a key reach the built site? (after npm run build)
grep -lE "sk-proj-|sk-ant-|sk_live_" dist/assets/*.js
AIza… keys are Google API keys. In a Firebase web config that key is public by design and protected by your security rules; elsewhere (a Gemini or Maps server key) treat it as a secret.
The fix
No code change makes a published key safe again. In this order:
- Rotate the key now at the provider: create a new one and revoke the old. Until you do, anyone who copied it can use it.
- Move the call to a server, usually a Supabase Edge Function, and keep the new key in a server-side secret:
supabase secrets set OPENAI_API_KEY=…. - Remove the key from the code, untrack the env file and ignore it. The old key stays in git history (PL-011), which is why step 1 comes first.
export const openai = new OpenAI({ apiKey: 'sk-proj-…', dangerouslyAllowBrowser: true });
const { data, error } = await supabase.functions.invoke('chat', { body: { messages } });
// supabase/functions/chat/index.ts (runs on the server)
import OpenAI from 'npm:openai@4';
const openai = new OpenAI({ apiKey: Deno.env.get('OPENAI_API_KEY')! });
// verify the caller first (see PL-017), then call openai.chat.completions.create(...)
# untrack and ignore the env file
git rm --cached .env
echo ".env*" >> .gitignore
echo "!.env.example" >> .gitignore
Code findings come with the file, the line and a one-line fix in the report, for your engineer to make. PatchLabs never pushes to your repo.
How PatchLabs finds it
[SCAN] current code src/lib/ai.ts:4 AI key sk-p…Q9xT LEAK .env:1 payment key sk_l…4f2a LEAK [ACTION] rotate both at the provider [RESCAN] current code → 0 keys OK
The key scan reads every file in the current code (and, for PL-011, the full history). It never uses a key it finds, so it can't tell you whether the key still works. The report shows each key masked, with its file and line, the kind of key and where to rotate it. The re-check confirms the key is gone from the current code; only you can confirm it was revoked.
When the pattern is intended
Publishable keys (Supabase anon or publishable, Stripe pk_…, a Firebase web config), test-mode keys such as sk_test_…, and placeholders in .env.example are fine to commit. The report shows the file for every key it finds, so these are quick to tell apart.
Why it’s rated high (depends on the key)
High by default, because a working secret key lets anyone act as your account with that provider. The severity follows the kind of key: a database connection string with its password reaches all your data, while a narrowly scoped key does less. The report names the key type so you can judge.
Related findings
Service role key in client code
The key that ignores every access rule, shipped to every visitor's browser.
PL-011Key still in git history
You deleted the key. Every old commit still has it, and so does every clone and fork.
PL-017Edge function open without sign-in while using the service key
A server function holding the key that ignores every rule, with nothing checking who is calling it.
Guides
- What to do in the first hour after a leak
- How to rotate a leaked API key and clean your git history
- Pre-launch security checklist for vibe-coded apps
Free check
Does your app have PL-009?
Paste your public repo. PatchLabs Code reads it for this and every other code finding, and the Lab rebuilds your database to test who can read what.
Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.