PL-009high (depends on the key)Code

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 and .env (vulnerable)
// 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

check-pl-009.sh
# 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:

  1. 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.
  2. 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=….
  3. 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.
the change
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

key scan · report excerpt
[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
synthetic exampleMasked, not real keys

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.

Guides

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.

Free for your first appReads your public repo onlyOr run the free static RLS check

Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.