no-browser-api-key-exposure
Forbid dangerouslyAllowBrowser, which exposes the OpenAI API key to the client
Forbid
dangerouslyAllowBrowser, which exposes the OpenAI API key to the client.
- CWE: CWE-522 โ Insufficiently Protected Credentials
- OWASP: A07:2021 โ Identification and Authentication Failures
- CVSS: 8.6 (High) ยท Recommended:
error
Why
The flag exists to let the SDK run in a browser. That means the API key is shipped to every visitor and readable in devtools or the network tab. A leaked key is billable by whoever finds it, and rotating it requires a deploy.
The rule fires only in files importing openai or @openai/, and only when the flag is true โ written inline or held in a const. A runtime-decided value or an unreadable spread could be false, so those are left alone. The Realtime WebSocket client (new OpenAIRealtimeWebSocket({ dangerouslyAllowBrowser: true }, client)) is covered too.
When the flag is not a leak
The flag is reported when it ships the app's own credential: a literal, an environment read (import.meta.env.VITE_โฆ and NEXT_PUBLIC_โฆ are inlined into the bundle), an imported or build-time-defined constant, or no key at all (the SDK then falls back to the environment variable). It stays silent when the browser obtains the credential per session, because then nothing of the app's ships:
- a token provider โ
azureADTokenProvider(the SDK turns the flag on itself when one is passed); - a key-provider function (
apiKey: async () => โฆ); - a key that is a parameter (the visitor's own key, bring-your-own-key) or the result of an
awaitor call โ an ephemeral token fetched from your server, which is how OpenAI's Realtime docs tell browser clients to authenticate.
A mixture still ships: userKey || import.meta.env.VITE_KEY reports, because the fallback is the app's key. The flag and the options are followed through same-file consts, and only a client bound to this SDK counts โ the same flag on another vendor's client in the same file is that vendor's plugin's finding.
Incorrect
import OpenAI from 'openai';
const client = new OpenAI({ apiKey: KEY, dangerouslyAllowBrowser: true });Correct
// server route
import OpenAI from 'openai';
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });Call the model server-side and forward the result, so the key never leaves the server.
Use this rule
npm install -D oxlint-plugin-openai-security{ "jsPlugins": ["oxlint-plugin-openai-security"], "rules": { "openai-security/no-browser-api-key-exposure": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-openai-security'], rules: { 'openai-security/no-browser-api-key-exposure': 'error', }, },});oxlint-plugin-openai-security re-exports eslint-plugin-openai-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
Did this rule catch something? Star the repo to get new CWE coverage as we ship it โ or follow the AI-code-security benchmarks behind these rules.