Hardcoded API key in JS on Next.js
An API key is hardcoded in your client-side JavaScript. Anyone who views your site source can extract it. The fix has three steps: (1) rotate the key immediately — assume it is compromised; (2) move the API call to your server (API route, server action, or serverless function) so the key never ships to the browser; (3) add guardrails — prefix only server secrets without NEXT_PUBLIC_, scan builds for leaked keys. Do not try to "obfuscate" the key — it will be found.
The fix for Next.js
Next.js — move to server
Create a Route Handler that calls the third-party API with the key. Your client calls your handler, never the third party directly.
// app/api/ai/route.ts — SERVER ONLY
import { NextRequest } from 'next/server';
export async function POST(req: NextRequest) {
const { prompt } = await req.json();
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, // no NEXT_PUBLIC_
'Content-Type': 'application/json',
},
body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }] }),
});
const data = await res.json();
return Response.json(data);
}
// Client calls /api/ai, never api.openai.com directly.Why it matters
Attackers run automated bundle-scanning tools against popular sites. An exposed OpenAI, Stripe, or cloud key is drained within hours of publication — sometimes minutes. Your bill can run to thousands of dollars before you notice.
Confirm the fix worked
Scan your Next.js site to confirm this finding is gone.
AI prompt
Apply across your codebase
Paste this into Cursor, Lovable, Bolt, v0, or Claude Code.
Find every API key in my client-side code. For each one: (1) tell me which service it belongs to so I can rotate it; (2) determine if the API call needs to happen in the browser (it usually does not); (3) move the call to a server-side route handler or server action that reads the key from an env var without NEXT_PUBLIC_ prefix; (4) update the frontend to call my endpoint instead. Provide the rotation URL for each service.FAQ
Frequently asked questions
- But the user needs to make the API call from their browser for latency.
- Then use a signed, time-limited credential (like S3 presigned URLs, Twilio Access Tokens, or OpenAI's ephemeral keys) generated server-side per-request. Never the raw master key.
- My framework variables all start with NEXT_PUBLIC_. Is that fine?
- For public values yes (Stripe publishable key, Supabase anon key). For secret values no — `NEXT_PUBLIC_` inlines the value into the browser bundle. Use `NEXT_PUBLIC_` only for values that are meant to be public.
Related fix guides
Fix these too
Exposed Stripe secret key
A leaked Stripe secret key means an attacker can charge your customers, refund payments, or drain your account. Rotate immediately.
Read moreExposed OpenAI API key
OpenAI keys in client code get drained fast — attackers use them to run expensive models on your bill. Here is how to lock it down.
Read moreExposed .env file
An exposed .env file is a critical leak — it contains API keys, database URLs, and secrets. Here is why it happens in vibe-coded apps and how to lock it down.
Read moreExposed Supabase service key
The service role key bypasses all security in Supabase. If it is in your client code, an attacker has full database access. Here is how to find and fix it.
Read moreFree tools