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.
Database connection strings
A postgres:// or mongodb+srv:// URL with its password in it, sitting in a file every visitor downloads. This is the most direct version of the whole problem: it is not scoped to a user, no policy stands behind it, and whoever copies it can read every table, change every row and drop the lot. We never show the password back to you, only the host, so you can tell which database it is. Tutorial examples like postgres://user:password@localhost are recognised and skipped, which is why documenting your setup on your own site does not light this up.
Slack, SendGrid, Twilio, npm, Resend, Mapbox and Algolia
The long tail, and the reason a scanner that only knows the famous five misses so much. A Slack bot token reads your private channels. A SendGrid or Resend key sends mail carrying your domain's reputation, which is why leaked ones become phishing within hours. An npm publish token can ship code to everyone who installs you. Two of these are graded deliberately low: a Slack webhook URL only writes into one channel, and a Twilio account identifier opens nothing on its own, so neither is dressed up as a breach. The Algolia check is phrased as a question rather than a verdict, because the search key and the admin key are identical strings and telling them apart would mean using the key, which a passive scan will not do.
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.