Everything in your bundle is public.
Enter a URL and we will fetch the JavaScript your page tells the browser to load, then read it for credentials. Not a guess from the HTML: the actual script files, the same ones anyone can open in devtools.
What this checks, and what a good answer looks like
Supabase service_role keys
The worst thing on this list. The service_role key bypasses every row level security policy you have written, so whoever holds it can read, change and delete every row in every table regardless of who they are logged in as. It looks identical to the publishable anon key, which is why it ends up in client code: someone hit a permissions error, swapped one key for the other, the error went away, and it shipped.
Supabase anon keys and Firebase config
These are meant to be public, and finding them is not a finding by itself. We report them because what they are worth depends entirely on your rules: an anon key with row level security switched off reads your whole database, and a Firebase config with rules left at allow read, write: if true does the same. The key is not the vulnerability. The key plus a missing policy is.
Stripe secret keys
A live sk_live_ key in client code means anyone can create charges, issue refunds and read your customer list. We flag test keys too, at lower severity: a sk_test_ in the bundle does not cost money, but it says the publishable and secret keys got mixed up once, and the same mistake tends to reach production eventually.
AWS access keys, GitHub tokens and private keys
An AWS key pair in a bundle is a bill and a breach at the same time, and these are the credentials automated scrapers hunt hardest for. A GitHub token exposes source and, if it carries write scope, the ability to push. A BEGIN PRIVATE KEY block in client code has no legitimate reason to be there at all.
Model provider keys
OpenAI, Anthropic and similar keys in client code are the newest entry on this list and increasingly the most common, because calling the model straight from the browser is the shortest path to a working demo. It is also a metered credential anyone can spend. This is the failure that turns into a four-figure bill overnight rather than a data breach.
Hardcoded authorization tokens
A literal Authorization: Bearer ... value baked into the bundle, usually left behind from testing against a real endpoint. It grants whatever the token grants, to everyone, for as long as it stays valid.
How to fix it
There is only one real fix and it has two halves: rotate the credential, because anything that reached a browser must be assumed to be in someone else's hands, then move the call that needed it to the server. The rotation is not optional and not something to defer. Keys in public bundles are found by automated scanners in hours.
# Anything with these prefixes is INLINED INTO THE BUNDLE at build
# time. Not read at runtime, not kept on the server: pasted into a
# file anyone can download. A secret here is a published secret.
NEXT_PUBLIC_* # Next.js
VITE_* # Vite, so also most Lovable and Bolt output
PUBLIC_* # Astro, SvelteKit
REACT_APP_* # Create React App
EXPO_PUBLIC_* # Expo
# Server-only names have no prefix, and your host's secret store is
# where they belong:
SUPABASE_SERVICE_ROLE_KEY=...
STRIPE_SECRET_KEY=...
OPENAI_API_KEY=...// app/api/summarise/route.ts
// The key stays on the server. The browser calls YOUR route, which
// is also the only place you can enforce who is allowed to ask.
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const session = await auth();
if (!session) return NextResponse.json({ error: "sign in" }, { status: 401 });
const answer = await fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: {
authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({ model: "gpt-5.4-mini", input: await request.text() }),
});
return NextResponse.json(await answer.json());
}// supabase/functions/admin-task/index.ts
// service_role belongs here and nowhere else. Deno.env reads the
// function's own secrets, which are never bundled to a client.
import { createClient } from "jsr:@supabase/supabase-js";
const admin = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);
Deno.serve(async (request) => {
const token = request.headers.get("authorization");
if (!token) return new Response("unauthorized", { status: 401 });
// Check who is asking BEFORE using a key that ignores every policy.
const { data: user } = await admin.auth.getUser(token.replace("Bearer ", ""));
if (!user) return new Response("unauthorized", { status: 401 });
return Response.json({ ok: true });
});1. Issue the new credential first, so nothing goes down.
2. Update your host's secret store and redeploy.
3. Revoke the old one. This is the step that actually helps, and
the one people skip because everything already works without it.
4. Check the provider's audit log for use you cannot account for.
5. Search your git HISTORY, not just your files:
git log -p -S 'sk_live_' -- . | head -50
Deleting a key from a file leaves it in every clone of the repo.This reads the scripts your page links to, which means it sees what a visitor's browser sees and no more. A key that only appears on a route we did not load, in a bundle behind a login, or in your git history rather than your current build, is invisible here and just as dangerous. Git history in particular is where most leaked keys actually live: removing a secret from a file does not remove it from the repository, and that is one of the first places a scan of the code itself looks.