浏览器里 API 密钥暴露了?2 分钟检查你的 OpenAI、Claude 或 Gemini 密钥

前端直接调用 AI 提供商会让密钥暴露在 DevTools 中。本文给出 2 分钟检查法、修复顺序,以及把调用移到服务端并测试的方法。

用 Apifox,节省研发团队的每一分钟

浏览器里 API 密钥暴露了?2 分钟检查你的 OpenAI、Claude 或 Gemini 密钥

免费使用 Apifox

相关推荐

最新文章

API

一体化协作平台

API 设计

API 文档

API 调试

自动化测试

API Mock

API Hub

立即体验 Apifox
目录

如果 Web 应用直接从浏览器调用 OpenAI、Claude 或 Gemini,你的 API 密钥就暴露在浏览器里,任何打开 DevTools 的人都能复制。2 分钟内可确认:打开 Network 标签页,发送一次 AI 请求,查找发往提供商的 Authorization、x-api-key 或 x-goog-api-key 请求头。如果看到自己的密钥,今天就轮换,并把调用移到服务端路由后面。

下文介绍检查、修复顺序,以及编码 Agent 如何测试修复后的路由。

为什么浏览器中暴露密钥是问题

浏览器中暴露的 API 密钥是随前端代码或网络流量发送的秘密,每个访问者都能读。如果浏览器发送密钥,用户就能复制并用你的账单调用提供商。

Google 的 Gemini API 密钥指南说“不要在 Web 或移动应用中硬编码 API 密钥”,因为“编译进客户端代码的密钥可被用户提取”,并建议“运行后端代理服务器发起实际 API 调用”。OpenAI 的 API 密钥安全指南规则相同:通过自己的后端路由请求,不要把密钥部署到客户端。

openai-node README 表示浏览器支持默认关闭,“以避免暴露秘密 API 凭据”,必须设置 dangerouslyAllowBrowser: true。Anthropic TypeScript SDK 使用相同选项名,并且只有请求携带 anthropic-dangerous-direct-browser-access: true 请求头时才接受浏览器 CORS 调用,见 Simon Willison 的记录。

这种模式在快速搭建的应用中很常见:最短路径是在 React 组件 fetch,密钥放在 VITE_ 或 NEXT_PUBLIC_ 变量。被盗密钥会被转卖用于批量推理,见被盗 AI API 密钥与 LLMjacking,你会在账单上发现。

如何检查(2 分钟)

2 分钟密钥暴露检查:1 在 Network 标签页查看 POST,2 在 JavaScript bundle 搜索 sk- 和 AIza,3 检查 NEXT_PUBLIC_ 或 VITE_ 环境变量

请针对生产环境检查,不要用 localhost。

  1. 查看 Network(45 秒)。 打开网站,按 F12(Mac 为 Cmd+Option+I),选 Network,触发一个 AI 功能,例如发送聊天消息。按 openai、anthropic 或 googleapis 过滤。如果请求直接发往 api.openai.com、api.anthropic.com 或 generativelanguage.googleapis.com,点击并打开 Headers。Authorization: Bearer、x-api-key、x-goog-api-key 或 ?key= 中出现密钥就是暴露。
  2. 搜索 bundle(45 秒)。 按 Cmd+Option+F(Windows 为 Ctrl+Shift+F)搜索所有已加载脚本,搜索 sk-、sk-ant-、AIza 和 dangerouslyAllowBrowser。提供给浏览器的 .js 文件中任何命中都是泄露,即使请求尚未发出。
  3. 检查环境变量前缀(30 秒)。 打开 .env 和托管控制台。构建工具会按设计复制部分变量:Vite 的 VITE_*(见 Vite 文档)、Next.js 的 NEXT_PUBLIC_*、Create React App 的 REACT_APP_*、Expo 的 EXPO_PUBLIC_*。这些前缀后的提供商密钥就是公开的。

终端扫描构建输出:

# 生产构建后,从项目根目录运行
grep -rnoE "sk-(proj-|ant-)?[A-Za-z0-9_-]{20,}|AIza[0-9A-Za-z_-]{35}" dist/ build/ .next/static/ 2>/dev/null
grep -rn "dangerouslyAllowBrowser" dist/ build/ .next/static/ 2>/dev/null

输出为空才通过。Google 表示 2026 年 5 月 28 日后创建的新 AI Studio 密钥是“auth keys”,格式可能不同,所以还要搜索 generativelanguage.googleapis.com。只有浏览器只与自己的域名通信(如 /api/chat),且任何地方不出现提供商主机名或密钥时,才算通过。

密钥暴露后的修复顺序

步骤 做什么 为什么 时间
1. 轮换 创建新密钥,吊销泄露的旧密钥 旧密钥可能已被复制 5 分钟
2. 移到服务端 把调用放进持有密钥的服务端路由或边缘函数 防止新密钥同样泄露 30–60 分钟
3. 加认证和限流 要求会话,限制每用户请求数 否则代理成为免费 AI 端点 30 分钟
4. 设置支出上限 在各提供商账单设置预算或硬上限 限制最坏账单 10 分钟
5. CI 扫描 输出 bundle 出现密钥模式就让构建失败 防止下一个功能改回去 15 分钟

先部署新密钥到服务端,再吊销旧密钥,否则功能会中断。零停机轮换见 API 密钥轮换最佳实践。OpenAI 建议设置强制执行的支出上限,因为告警本身不能停止流量;Google Cloud 预算会提醒,但不会停止使用。

把调用移到服务端路由

对比不安全与安全设置:不安全设置中浏览器持有密钥并直接调用提供商,DevTools 可复制;安全设置中浏览器调用服务端路由,路由持有密钥并应用认证、限流和支出上限

后端代理路由接收用户提示词,在服务端加密钥,调用提供商,只返回答案。浏览器永远看不到密钥。下面是 Next.js OpenAI 路由;Claude 和 Gemini 结构相同:

// app/api/chat/route.ts(仅在服务端运行)
import OpenAI from "openai";
import { getSession } from "@/lib/auth";        // 你的认证辅助函数
import { checkRateLimit } from "@/lib/limits";  // 你的按用户限流器

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // 没有 NEXT_PUBLIC_ 前缀

export async function POST(req: Request) {
  const session = await getSession(req);
  if (!session) return new Response("Unauthorized", { status: 401 });

  if (!(await checkRateLimit(session.userId, 20))) {   // 每分钟 20 次
    return new Response("Too many requests", { status: 429 });
  }

  const { message } = await req.json();
  if (typeof message !== "string" || message.length > 4000) {
    return new Response("Bad request", { status: 400 });
  }

  const response = await client.responses.create({
    model: process.env.OPENAI_MODEL!,   // 在服务端固定模型
    input: message,
  });

  return Response.json({ reply: response.output_text }); // 只返回答案
}

为什么安全而不只是隐藏:

  • 模型由服务端决定。 否则攻击者会选最贵的。
  • 输入有上限。 不能通过你的账户发送超大提示词。
  • 只返回答案。 不返回提供商原始响应或错误对象。
  • 没有公开前缀。 OPENAI_API_KEY 留在服务端;NEXT_PUBLIC_OPENAI_API_KEY 会发给每个访问者。

Claude 和 Gemini 使用各自 SDK 与 ANTHROPIC_API_KEY 或 GEMINI_API_KEY。Google 的网站和 IP 限制有帮助,但任何有 curl 的人都能发送任意 Referer,所以当作第二层。浏览器调用唯一合理的场景是 bring-your-own-key 工具,每个用户粘贴自己的密钥。

让编码 Agent 用 Apifox CLI 测试代理路由

移动密钥只是一半;另一半是证明下一个功能上线后它仍在服务端。让构建路由的 Agent 用 Apifox CLI 测试。先配置:

npm install -g apifox-cli@latest
apifox login --with-token <YOUR_TOKEN>
npx -y skills add https://apifox.com   # 按提示选择你的 Agent

给 Agent 一段提示:

Create Apifox test cases in project <id> for POST /api/chat on our staging
environment that assert:
1. a request without a session cookie returns 401,
2. a valid request returns 200 and the body does not contain "sk-", "sk-ant-",
   "AIza" or "api_key",
3. the 21st request inside 60 seconds from the same user returns 429.
Run them and summarize any failures.

Agent 读取 schema、验证 payload、创建并运行:

apifox test-case category --project <id>
apifox cli-schema get test-case-create
apifox cli-schema validate test-case-create --file case.json
apifox test-case create --project <id> --file case.json
apifox run --test-case <caseId> --project <id> -e <envId> -r cli,json

第二条断言最关键:提供商错误对象或调试字段一旦回显密钥,它就会失败。把 apifox run ... -r junit 放进流水线,每次部署重新检查;Apifox CLI CI/CD 指南有配置,Claude Code 中的 Apifox CLI 则覆盖 Agent 侧。Apifox 免费版不限成员数量,小团队无需额外购买席位即可运行。

仓库侧的问题——密钥提交到 Git 而不是发到浏览器——见用密钥扫描器检查泄露。如果 Agent 调用处理客户数据的工具,测试 AI Agent 数据泄露覆盖下一层。

让密钥持续暴露的常见错误

  • Base64 编码或拆分密钥。 浏览器必须重建才能发送,DevTools 会显示真实值。
  • 移到 localStorage 或 cookie。 仍在客户端。
  • 没有认证的代理。 公开 /api/chat 就是记在你账单上的开放 AI 端点。
  • “以防万一”保留旧密钥。 公开一天的密钥已被复制。
  • 忘记移动应用。 Google 指南把 Web 和移动应用放在一起是有原因的。

常见问题

如何知道 OpenAI 密钥是否暴露? 打开 DevTools Network,触发 AI 功能。发往 api.openai.com 且带 Authorization: Bearer sk-... 的请求就是暴露。还要在脚本中搜索 sk- 和 dangerouslyAllowBrowser。

放在 NEXT_PUBLIC_ 或 VITE_ 变量中安全吗? 不安全。这些前缀就是为了把值复制到客户端 bundle。请用无前缀的仅服务端变量,并在服务端路由读取。

限制 Gemini 密钥到我的网站就安全吗? 有帮助,但浏览器外脚本可发送任意 Referer。Google 自己也建议客户端应用使用后端代理。

发现密钥暴露,先做什么? 创建新密钥,部署到服务端,再吊销旧密钥。然后把调用移到带认证和按用户限流的路由,并设置支出上限。

编码 Agent 能帮我修复并测试吗? 能。让它把调用移到服务端路由,再用 Apifox CLI 创建并运行测试:无会话返回 401、响应无密钥模式、超过限流返回 429。