An AI API key accessible from client-side code is not a secret—it is a public credential embedded in a bundle downloaded by every browser that visits your site. CWE-312 (cleartext credential storage) and CWE-798 (hardcoded credential) both apply. OWASP A02:2021 (Cryptographic Failures) covers this class of exposure. Any attacker can extract the key from browser DevTools in under 30 seconds and use it to: exhaust your API quota (generating costs you bear), extract your system prompt by calling the provider directly, or abuse the provider's API for their own applications at your expense. In Next.js, prefixing any environment variable with NEXT_PUBLIC_ embeds its value in the client bundle at build time—this is the most common accidental exposure vector in production deployments.
High because client-side key exposure requires no attack sophistication—the credential is readable in plain text from browser DevTools, making exploitation trivial and immediate once discovered.
All AI API calls must be proxied through server-side routes. The AI SDK is never imported in client components, and the API key environment variable never carries a NEXT_PUBLIC_ prefix.
// Correct: server-side only (src/app/api/chat/route.ts)
import OpenAI from 'openai'
// Note: no NEXT_PUBLIC_ prefix — this variable is never in the client bundle
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
export async function POST(req: Request) {
// All AI calls happen here, server-side
}
// Client component (src/components/ChatWidget.tsx)
// No OpenAI import — just a fetch to your own API route
async function sendMessage(message: string) {
return fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ message })
})
}
If you have an existing NEXT_PUBLIC_OPENAI_API_KEY in .env or .env.example, rotate the key immediately before removing the prefix—it should be treated as compromised.
ID: ai-prompt-injection.system-prompt-protection.api-key-server-only
Severity: high
What to look for: List all LLM API key references across the codebase. For each, check where AI API calls are made. Look for AI SDK imports in client-side files (components, browser-side utilities, files without server-only markers). Check whether the AI API key environment variable is referenced in files that could be bundled into the client. In Next.js, look for NEXT_PUBLIC_ prefix on AI API key environment variables (this exposes them to the browser). Check layout.tsx, client components ('use client'), and browser-executed scripts for AI provider client initialization.
Pass criteria: All AI API calls are made server-side only (in API route handlers, server actions, server components, or backend services). The AI API key is accessed only from server-side code and is not prefixed with NEXT_PUBLIC_ or equivalent client-exposure prefix — 100% of LLM API keys must be server-side only, never in client bundles. Report: "X API key references found, all Y server-side only."
Fail criteria: AI SDK is initialized or called in client-side code, OR the AI API key environment variable has a NEXT_PUBLIC_ prefix (or equivalent), OR the API key is hardcoded in any file.
Skip (N/A) when: No AI provider integration detected.
Cross-reference: The Security Hardening audit secrets-not-committed check covers broader secrets management.
Detail on fail: "OpenAI client initialized in components/ChatWidget.tsx (a client component) — API key is accessible in browser" or "NEXT_PUBLIC_OPENAI_API_KEY used in .env.example — this exposes the key to all browser users"
Remediation: Exposing your AI API key client-side means anyone can use your API quota. All AI calls must be proxied through your server:
// Correct: server-side only (app/api/chat/route.ts)
import OpenAI from 'openai'
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }) // no NEXT_PUBLIC_
// Client component just calls your API route
// fetch('/api/chat', { method: 'POST', body: JSON.stringify({ message }) })
For a broader analysis of secrets management and API key hygiene, the Security Headers & Basics Audit covers this in the Basic Hygiene category.