# AI API key is not accessible client-side

- **Pattern:** `ab-000186` (`ai-prompt-injection.system-prompt-protection.api-key-server-only`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000186
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000186)

## Why it matters

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.

## Severity rationale

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.

## Remediation

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.

```ts
// 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
}
```

```ts
// 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.

## Detection

- **ID:** `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:

  ```ts
  // 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.

---

## External references

- cwe CWE-312
- cwe CWE-798
- owasp A02
- nist-ai-rmf GOVERN 1.1

Taxons: cryptography-and-secrets

HTML version: https://auditbuffet.com/patterns/ab-000186
