VibeSecurity

Fix

Fix: secret exposed in a NEXT_PUBLIC_ or VITE_ variable

The NEXT_PUBLIC_ and VITE_ prefixes are instructions to the build tool: put this value into the browser code. A secret stored under either prefix is published to every visitor.

By the VibeSecurity team3 min read

What this means

Next.js documents that variables prefixed with NEXT_PUBLIC_ are inlined into the JavaScript bundle at build time, replacing process.env references with the hard-coded value. Vite documents that VITE_ variables are exposed in client-side source code, and that they should not contain sensitive information such as API keys. So the value is in a file anyone can download. Public identifiers are fine there. Secret keys are not.

Why it happens

  • The code ran in the browser and the variable came back undefined. Adding the prefix made the error go away, and AI tools suggest exactly that.
  • The API call was written in a client component, so the key had to be available in the browser for the code to work.
  • Confusion between a provider's two keys: a publishable key (made for browsers) and a secret key (made for servers).
  • A single .env was copied from a tutorial where every variable had the prefix.

How to fix it

  1. 1Work out whether the value is truly secret. Safe in the browser: Supabase URL and publishable or anon key, Firebase web config, Stripe publishable key, analytics ids. Never in the browser: Supabase service_role or secret keys, Stripe secret keys, OpenAI or other AI provider keys, database URLs, JWT signing secrets, email and SMS provider keys.
  2. 2Rotate the secret at the provider. The old value has been public since the first deploy that contained it.
  3. 3Rename the variable without the prefix, in your .env files and in your hosting dashboard.
  4. 4Move the code that uses it into a server route, server action or edge function. The browser calls your route. Your route calls the provider.
  5. 5Protect the new route: require a signed-in user and add rate limiting, otherwise you have built a free proxy to a paid API.
  6. 6Rebuild and redeploy. The value is baked in at build time, so changing the variable alone does not clean the bundle already deployed.
  7. 7If any .env file with the key was committed, purge it from git history with git filter-repo after rotating.
Before and after: move the key to a server route
NEXT_PUBLIC_OPENAI_API_KEY=sk-...

OPENAI_API_KEY=sk-...

export async function POST(request: Request) {
  const user = await getSignedInUser(request);
  if (!user) return new Response('Unauthorised', { status: 401 });

  const { prompt } = await request.json();
  const upstream = await fetch('https://api.openai.com/v1/responses', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    },
    body: JSON.stringify({ model: 'YOUR_MODEL', input: String(prompt).slice(0, 2000) }),
  });
  return new Response(upstream.body, { status: upstream.status });
}

How to confirm the fix

  • Build locally and search the output for the start of the old and new key values. Neither should appear in browser files.
  • On your live site, open devtools, Sources, and use search across all files for the key's prefix (for example sk- or sb_secret_).
  • List every public variable in your project and check each one against the safe list above.
  • Call the provider with the old key. It should be rejected.
Shell: search your own build and list public variables
npm run build
grep -rIl 'FIRST_12_CHARS_OF_THE_KEY' .next/static dist build 2>/dev/null

grep -rhoE '(NEXT_PUBLIC|VITE)_[A-Z0-9_]+' src app .env* 2>/dev/null | sort -u

Frequently asked questions

Can I hide the key by obfuscating or encoding it?

No. If the browser can use the key, a person can extract it from the code or watch it in the Network tab. The only fix is to keep it on a server.

Is it safe to put my Supabase anon key in NEXT_PUBLIC_?

Yes. It is designed to be public and depends on row-level security for protection. The service_role or secret key is the one that must stay on the server.

I removed the prefix but the key is still in my live bundle. Why?

Values are inlined at build time. Trigger a fresh build and deploy, and remember older bundles may stay cached for a while. Rotation is what makes those copies harmless.

Sources

  1. 1.Next.js: Environment variables
  2. 2.Vite: Env variables and modes
  3. 3.Supabase: Understanding API keys
  4. 4.Vercel: Environment variables
  5. 5.GitHub: Removing sensitive data from a repository