AI5 min read

How to Find API Keys Leaked in Your JavaScript Bundle

Every key your browser code uses is readable by every visitor. Here is how secret keys end up in a production bundle, how to search for them in five minutes, and which keys are actually fine to ship.

By UseShipReady

Any API key your browser code uses ends up in the JavaScript you ship, where any visitor can read it in DevTools. To find leaks, build your app and search the output for known key prefixes such as sk-, sk-ant-, sk_live_ and AKIA. Publishable keys (Stripe pk_live_, the Supabase anon key, the Firebase apiKey) are designed to be public. Secret keys are not: move the call to a server route, then rotate the key.

There is no way to hide a secret in frontend code. Minification, obfuscation and environment variables all change how the key looks in your editor. None of them change the fact that the browser needs the real value to use it, so the real value is in what the browser downloads.

How do secret keys end up in a frontend bundle?

A browser-exposed environment variable prefix

Next.js inlines any variable starting with NEXT_PUBLIC_ into the client bundle at build time. Vite does the same for VITE_, Create React App for REACT_APP_, and Expo for EXPO_PUBLIC_. The prefix is an instruction: publish this value. When an assistant is asked to "make the OpenAI key available in the component" and the plain variable comes back undefined, adding the prefix is the one-line change that makes the error go away, and it publishes the key.

Calling a paid API directly from the browser

The OpenAI JavaScript SDK refuses to run in a browser unless you pass dangerouslyAllowBrowser: true. The name is the warning. Enabling it means the key travels to every visitor, who can then use your account for their own requests until you notice the bill.

A key pasted straight into the code

The simplest route: a key hardcoded while prototyping "just to test it", in a file that is later imported by a client component. It survives because nothing breaks.

How do I search my bundle for leaked keys?

Search the build output, not your source. Source contains variable names; the build contains values. Build for production locally, then grep the client output directory:

shell
npm run build

# Next.js: .next/static   Vite: dist   CRA: build
grep -rEo "(sk-(proj-|ant-|or-)?[A-Za-z0-9_-]{20,}|sk_live_[A-Za-z0-9]{10,}|rk_live_[A-Za-z0-9]{10,}|whsec_[A-Za-z0-9]{10,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{20,}|xox[bp]-[A-Za-z0-9-]{10,}|SG\.[A-Za-z0-9_-]{20,}|re_[A-Za-z0-9_]{20,}|gsk_[A-Za-z0-9]{20,}|hf_[A-Za-z0-9]{20,}|r8_[A-Za-z0-9]{20,})" .next/static dist build 2>/dev/null | sort -u

To check what is live rather than what your laptop builds, open your production site, go to DevTools, then Sources, and use "Search all files" for the same prefixes. The Network tab is worth a look too: a request going straight from the browser to api.openai.com or api.anthropic.com means the key is in the page.

Which API keys are safe to expose?

Some keys are built to be public. They identify your project, and the provider enforces limits somewhere else. Treating them as secrets wastes time; treating secret keys like them is how accounts get drained.

Designed to be publicMust stay on the server
Stripe publishable key (pk_live_)Stripe secret (sk_live_), restricted (rk_live_) and webhook (whsec_) keys
Supabase anon / publishable keySupabase service role / secret key
Firebase config apiKeyFirebase Admin SDK service-account JSON
Mapbox public token (pk.)Mapbox secret token (sk.)
Google Maps browser key, with referrer restrictionsAny Google Cloud service-account key
None for AI providersOpenAI, Anthropic, OpenRouter, Groq, xAI and similar keys
Public identifiers versus secret keys

A public key is only safe while the protection behind it is in place. The Supabase anon key is fine to publish when Row Level Security is configured, and dangerous when it is not, which is the subject of whether the Supabase anon key is safe to expose.

How do I fix an API key exposed in JavaScript?

  1. Rotate the key at the provider first. Removing it from the code does not un-publish the copy already downloaded, cached and indexed.
  2. Move the call into a server route (a Next.js Route Handler, a server action, an Edge Function or a small API endpoint) that reads the key from a server-only environment variable with no public prefix.
  3. Have the browser call your route instead of the provider. Your route decides which requests are allowed.
  4. Add authentication and rate limiting to that route. Otherwise you have replaced a leaked key with an open proxy that spends the same money.
  5. Set a monthly spend limit at the provider, so a mistake is capped even if the other steps fail.
  6. Rebuild, then repeat the grep above to confirm the key is gone.
typescript
// app/api/chat/route.ts — the key never leaves the server
import OpenAI from 'openai'

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

export async function POST(req: Request) {
  // Authenticate the user and apply a rate limit here.
  const { message } = await req.json()
  const reply = await openai.responses.create({ model: 'gpt-5-mini', input: message })
  return Response.json({ text: reply.output_text })
}

What happens after an API key leaks?

Leaked keys are found by automated scanners that crawl public JavaScript and code repositories looking for exactly these prefixes, so assume discovery within hours rather than weeks. What follows depends on the key:

  • AI provider keys are resold or used to run someone else’s workload, and the usual first sign is a usage spike or a billing alert. Some providers detect publicly leaked keys and disable them automatically, which you may first experience as your own app breaking.
  • Payment secret keys expose customer records and let someone issue refunds or read payment history.
  • Email and messaging keys are used to send spam or phishing from your verified domain, which damages deliverability long after the key is rotated.
  • Cloud keys are used to start compute billed to your account, and to read whatever storage the key can reach.

After rotating, read the provider’s usage or audit log back to the date the key first shipped. If you see activity you did not cause, contact the provider’s support: many will review fraudulent usage on a key that was demonstrably abused, but only if you ask.

The same reasoning applies to secrets in files rather than in code. If your deployment serves the project folder, an exposed .env file leaks every key at once, and no amount of bundle hygiene helps.

How UseShipReady finds keys in your bundle

UseShipReady downloads the JavaScript your pages actually load and matches it against the formats of known secret keys, including OpenAI, Anthropic and Stripe secret keys. It separately reports an AI provider client configured to run in the browser, and a secret published through a browser-exposed environment variable. Findings show a redacted fragment of the key and the file it was found in, never the full value.

Publishable keys are deliberately not reported: a finding for a Stripe pk_live_ key would be noise, and noise teaches people to ignore the report. For the broader picture of what AI assistants tend to get wrong, read the security risks of AI-generated code.

Frequently asked questions

Can I hide an API key in a React app?
No. Anything the browser uses to make a request, the visitor can read. The only way to keep a secret key secret is to never send it to the browser: make the request from a server route that holds the key.
Are environment variables in Next.js secure?
Variables without the NEXT_PUBLIC_ prefix stay on the server and are safe for secrets. Variables with the prefix are copied into the client bundle at build time and are public. The prefix, not the .env file, decides.
Is it enough to delete the key from my code?
No. The old bundle may still be cached by CDNs and browsers, and anyone who copied the key still has it. Rotate the key at the provider, then remove it from the code.
Is the Firebase apiKey a secret?
No. Firebase documents it as a project identifier that is safe to include in client code. Access to your data is controlled by Firebase Security Rules, which is where the real protection has to be.

Sources

Related reading

Is your site ready to ship?

UseShipReady scans up to ten pages for security, AI exposure, email deliverability, SEO and launch readiness — with a paste-ready fix for each finding. Free, no signup.