MLVibeScan

Finding explained

API key in the frontend: what the finding means

If an API key sits in your shipped JavaScript, every visitor to your site can read it — the browser’s developer tools are all it takes. Automated bots comb publicly reachable bundles for exactly these keys and use them within minutes. So the most important step is not removing the key from your code but revoking it at the provider: it counts as compromised the moment it shipped, and a new bundle does not undo the old one.

The scan is free and takes about twenty seconds. You see straight away how many findings there are and how serious they are.

Scan your own app for free

Do it in this order

First: revoke the key at the provider. Now, not after the rewrite. As long as it is valid it can be used — and the bill goes to you.

Second: issue a new key and put it somewhere that does not ship — in your server’s or function’s environment variables, not in a file with a `VITE_` prefix.

Third: move the call. From now on the frontend calls your own route, and that route talks to the provider. The browser never sees the key.

Fourth: check your billing. If the key was public for a while, look at the provider’s usage view for anything that did not come from you.

How it got there in the first place

Almost always via the `VITE_` prefix. Vite bakes every variable with that prefix into the bundle at build time — that is what the prefix is for. The name says nothing about whether the value was meant to be secret.

With Next.js the same applies to `NEXT_PUBLIC_`. Both prefixes literally mean “this may be public”, and both are regularly used for values that may not.

The second route is the SDK itself. `new OpenAI({ apiKey, dangerouslyAllowBrowser: true })` works — the library is telling you in the name of the option that this was never intended.

Which keys may live in the frontend?

Public and harmless: Supabase `anon`, Firebase configuration, Stripe `pk_` keys, Google Maps keys with a referrer restriction set. These values identify your project, not a permission.

Never in the frontend: OpenAI and Anthropic, Stripe `sk_`, Supabase `service_role`, AWS credentials, SMTP passwords — anything called “secret” or “service”.

Google keys have a classic failure mode: the key is meant for the frontend, but has no restriction to your domain. Then anyone can use it on their own site, and the billable calls run through your account.

The call belongs on the server

The pattern is the same for every provider: a small route of your own that holds the key and forwards the request. A Supabase edge function, a Vercel route under `api/`, a Netlify function.

Do not forget to protect that route. Otherwise you have hidden the key but anyone can still spend it on your behalf — just via your detour.

// api/chat.ts — the key stays on the server
export default async function handler(req, res) {
  // Check WHO is asking first. Without this the route is
  // a public doorway to your quota.
  const user = await checkAuth(req);
  if (!user) return res.status(401).json({ error: 'Not signed in.' });

  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
      'content-type': 'application/json',
    },
    body: JSON.stringify(req.body),
  });

  res.status(response.status).json(await response.json());
}

Is rebuilding the bundle enough?

No. A new bundle removes the key from the current file — but the old one was publicly reachable, often for weeks. It may sit in archive services, in browser caches, in the databases of the bots that collected it.

That is why revoking is the actual step. Everything else only prevents a repeat.

What we check automatically

  • Key patterns in the shipped JavaScript
  • Google key without restriction (deep scan)
  • Reachable .env and .git files

Frequently asked questions

How quickly is a public key abused?
Faster than people assume. Automated collectors scan public JavaScript bundles and code repositories for key patterns. For well-known formats — OpenAI’s `sk-` prefix, for instance — the gap between publication and the first foreign call is routinely minutes.
I removed the key. Do I still have to revoke it?
Yes. A key that has shipped counts as compromised. Removing it prevents the next shipment, not use by someone who already has it.
The key is in my .env — isn’t that safe?
That depends on the prefix. A `.env` is read at build time, and anything with `VITE_` or `NEXT_PUBLIC_` is baked into the bundle. The file itself is not shipped, but its contents very much are. Also check whether the `.env` accidentally sits in your public directory — that is a finding of its own.
How do I find every key in my app?
By hand: open the largest bundle in the developer tools and search for `sk-`, `sk_live`, `service_role` and `AKIA`. Automatically: our scan checks the shipped JavaScript against roughly sixty key patterns from different providers.