Leaked API keys in Next.js — NEXT_PUBLIC_ prefix + client-bundle audit
NEXT_PUBLIC_-prefixed env vars ship in the client bundle. Server secrets accidentally prefixed = bundled credentials shipped to every visitor. Here's the detection + fix.
If a server secret is prefixed NEXT_PUBLIC_, it ships in your client JS bundle. Every visitor downloads the secret. This guide walks the audit + fix.
What it is
Next.js distinguishes server-only env vars (no prefix) from public env vars (NEXT_PUBLIC_ prefix). Public-prefix vars are inlined into client JavaScript at build time. Mixing them up = secrets in the bundle.
Vulnerable example
// .env.local
NEXT_PUBLIC_OPENAI_API_KEY=sk-proj-XXX // WRONG — ships to client
// app/components/Chat.tsx
const response = await fetch("https://api.openai.com/v1/chat/completions", {
headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}` }
});Fixed example
// .env.local
OPENAI_API_KEY=sk-proj-XXX // server-only
// app/api/chat/route.ts (server)
export async function POST(req: Request) {
const { message } = await req.json();
const r = await fetch("https://api.openai.com/v1/chat/completions", {
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
method: "POST", body: JSON.stringify({ messages: [{ role: "user", content: message }] }),
});
return Response.json(await r.json());
}How Securie catches it
.env.local:12Leaked API keys in Next.js
Securie's secrets specialist's live_validate step probes every NEXT_PUBLIC_-prefixed var; if the value matches a known secret pattern (sk-proj-, sk-ant-, sk_live_), critical-severity finding before merge.
// .env.local
OPENAI_API_KEY=sk-proj-XXX // server-only
// app/api/chat/route.ts (server)
export async function POST(req: Request) {
const { message } = await req.json();
const r = await fetch("https://api.openai.com/v1/chat/completions", {
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
method: "POST", body: JSON.stringify({ messages: [{ role: "user", content: message }] }),
});
return Response.json(await r.json());
}Checklist
- No NEXT_PUBLIC_ prefix on secrets
- Run grep -rE 'NEXT_PUBLIC_.*KEY' on .env files quarterly
- Proxy paid-API calls server-side via app/api routes
- Per-key spend caps as backstop
- Securie reviews on every PR
FAQ
Can I use NEXT_PUBLIC_ for the publishable Stripe key?
Yes — pk_live_ / pk_test_ are designed to be public. Check the vendor docs for which keys are publishable.
What if I accidentally committed?
See /leak/<vendor> for vendor-specific rotation.
Related guides
Vercel environment variables have three flavors (development, preview, production) and two scopes (server-only and NEXT_PUBLIC_). Mixing them up leaks production secrets. Here is the rule and the canonical bugs.
Per-platform env-var setup with NEXT_PUBLIC_ guidance, secret manager recommendations, rotation cadence.
Not in .env files. Not in localStorage. Here is the 2026 guide to storing and accessing secrets in a small-team Node.js / Python app.
Rotating an API key without taking your app down requires a specific dual-read single-write sequence. Here is the exact pattern.