ChillScan

← Blog

How to Find Leaked API Keys in Your JavaScript Bundle

Everything your frontend ships, anyone can read. If an AI assistant wired a secret key straight into your app, it's sitting in a JS file right now. Here's how to check in a few minutes, no tools beyond your browser.

TL;DR

Open your live site → DevTools → search all files for sk-, sk_live_, AKIA, sb_secret_, eyJ (decode it and check for a service_role claim) and AIza. Publishable keys are fine. Secret keys are not: rotate them, move the call to a server, and have your frontend talk to that server instead.

1. Open DevTools on your live site

Check the deployed site, not your local dev server — the live bundle is what attackers see.

  • Right-click anywhere → Inspect (or F12 / Cmd+Option+I).
  • Go to the Sources tab (Chrome/Edge) or Debugger (Firefox).
  • Press Ctrl+Shift+F (Cmd+Option+F on Mac) to search across every loaded file.
  • Also peek at the Network tab: reload, filter by JS or Fetch/XHR, and look at request headers — a key sent as Authorization: Bearer sk-… from the browser is a leak too.

Click around a bit first (log in, open a few pages) so lazy-loaded chunks get loaded and searched.

2. Search for these patterns

Search forWhat it usually is
sk-OpenAI secret keys (sk-proj-…). Anthropic keys look like sk-ant-…
sk_live_Stripe secret key (live mode). sk_test_ is test mode — still rotate it.
rk_live_Stripe restricted key. Safer than a full secret, but still server-only.
AKIAAWS access key ID. If there's a secret access key next to it, that's the real problem.
sb_secret_Supabase secret key (newer key format). Admin access that bypasses Row Level Security. sb_publishable_ keys are fine.
eyJA JWT. Legacy Supabase keys are JWTs, and the word service_role is base64-encoded inside, so a plain search for service_role usually won't find it. Decode the token locally and check its role claim: anon is fine, service_role is the admin key.
AIzaGoogle API key (Firebase, Maps, etc.). Often fine — see below.

Bonus searches: service_role (catches code or variable names that mention it), secret, api_key and Bearer. Expect some false positives from library code — what you care about is a long, random-looking string sitting right next to the match.

3. Public by design vs never safe

Finding a key isn't automatically bad. Some keys are meant to live in the browser:

Fine to be public

  • Supabase anon / publishable key — as long as RLS is on for every table.
  • Stripe publishable key (pk_live_, pk_test_).
  • Firebase web config (apiKey: "AIza…") — it identifies your project; Security Rules do the protecting. Restrict it by domain/API in Google Cloud anyway.

Never safe in the browser

  • OpenAI / Anthropic secret keys — anyone can run up your bill.
  • Stripe secret key (sk_live_) — refunds, customer data, the works.
  • AWS secret access key — access to whatever that IAM user can touch.
  • Supabase service_role key — bypasses all your RLS.

Found a service_role key? We wrote a whole guide: Supabase service_role key exposed? Don't panic, do this →

4. Rotate the leaked key first

If a secret was ever in a public bundle, assume someone has it — deleting it from the code doesn't un-leak it. Old deploys, CDN caches and git history stick around.

  • Create a new key in the provider's dashboard (OpenAI, Anthropic, Stripe, AWS IAM, Supabase).
  • Revoke or delete the old one. (Exception: a legacy Supabase service_role JWT can't be revoked on its own; see the Supabase guide below.)
  • Check the provider's usage/billing and logs for activity you don't recognize.

5. Move the secret to the server

Put the new key in a server-only env var on your host. No NEXT_PUBLIC_, VITE_, REACT_APP_ or PUBLIC_ prefix — those literally mean "bundle this for the browser." Only read it from server code: API routes, server actions, edge functions.

6. Add a small proxy endpoint

Your frontend still needs the feature, so give it a tiny server route that makes the call on its behalf. The browser talks to your endpoint; only your server talks to the provider.

// app/api/chat/route.ts (Next.js sketch)
export async function POST(req: Request) {
  // 1. check the user is logged in (and rate-limit them)
  const { prompt } = await req.json();

  const res = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, // server-only
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "your-model",
      messages: [{ role: "user", content: prompt }],
    }),
  });

  return new Response(res.body, { status: res.status });
}

Add auth and a rate limit so your proxy doesn't become a free, public relay for someone else. On Supabase, an Edge Function with the key stored as a function secret does the same job.

Let ChillScan do the searching

Don't feel like digging through minified JS? Paste your URL into the free scan and we'll check the page HTML and the first-party JavaScript files it loads for leaked keys (including decoding Supabase JWTs to spot service_role keys), plus headers, HTTPS, SEO and speed. No signup. We don't run your JavaScript or click around, so keys that only load after login or in lazily loaded chunks can be missed; the DevTools search above still covers those.

Supabase service_role key exposed? →

Built on Lovable? Lovable security checklist →

The full launch checklist →