← Back to the library
SecurityCore · 7 min

An API key ended up in the repo or the browser bundle

“A provider emails about an exposed key, push protection blocks a commit, or someone finds a live secret in git history or in the minified JavaScript the site ships.”

THE PRINCIPLE

Deleting a secret does not un-leak it. Rotation is the fix; history rewriting is housekeeping.

FIRST MOVES

  1. Revoke or rotate the key at the provider now. For zero downtime, issue a new key, deploy it, then revoke the old one.
  2. Check the provider's usage and audit logs for calls you did not make since the first exposure.
  3. Move the call behind your backend or an edge function. VITE_ and NEXT_PUBLIC_ variables are inlined into the bundle, so treat them as public.
  4. Enable secret scanning and push protection on the repository, and add a pre-commit scanner for local feedback.
  5. Rewrite history only after rotation, and coordinate it: forks, clones, CI caches, and PR refs may still hold the old commits.

TOOLS: THEN → NOW

Manual review to catch committed keysGitHub secret scanning with push protection, plus a pre-commit scanner
Keys in client env filesServer-only env vars behind a backend or edge proxy; client-prefixed variables reserved for public values
Long-lived static keys in CI settingsShort-lived credentials via OIDC workload identity and a secrets manager

PATTERN SNAPSHOT

// The browser calls /api/geocode; the key stays on the server.
// .env (no VITE_ / NEXT_PUBLIC_ prefix): MAPS_API_KEY=...
// Authenticate and rate-limit this route, or it becomes a free proxy for the key.
export async function GET(req: Request) {
  const q = new URL(req.url).searchParams.get("q") ?? "";
  const upstream = new URL("https://maps.example.com/v1/geocode");
  upstream.searchParams.set("q", q.slice(0, 200));
  const res = await fetch(upstream, {
    headers: { Authorization: `Bearer ${process.env.MAPS_API_KEY}` },
  });
  return new Response(res.body, { status: res.status, headers: { "content-type": "application/json" } });
}

CLOSE THE AI. EXPLAIN THIS.

You force-pushed a commit that removes a leaked key from history. Why is the key still compromised, and what should you have done first?

HOW IT WORKS UNDERNEATH

SOURCES

Guide reviewed