239 个模型的密钥,一个硬编码变量

大模型众测报告里最刺眼的例子:某产品把云平台 239 个模型的 API Key 硬编码在前端。密钥泄露不是新问题,但大模型把它放大了——一个 key 背后是一整片模型和一张账单。这篇讲怎么扫、怎么管、怎么轮换。

图:gitleaks 仓库首页,用于扫描代码与 Git 历史中的密钥(来源:github.com/gitleaks/gitleaks,截图于 2026-10-19)

图:gitleaks 仓库首页,用于扫描代码与 Git 历史中的密钥(来源:github.com/gitleaks/gitleaks,截图于 2026-10-19)

众测报告里有一句话我看了很久:某大模型存在 API 密钥硬编码与泄露漏洞,暴露了所有用户共享使用的云平台 239 个模型的 API 密钥。

239 个模型,一组 key。这意味着任何拿到这组 key 的人,可以免费调用 239 个模型,账单记在厂商头上;如果这些模型里包含微调过的私有模型,还可能被逆向、被滥用、被拿去训练竞品。

密钥是怎么跑到前端去的

我见过和听过的方式,基本就这几种:

  • 前端直连:为了省一个后端代理,把 key 写进 VITE_* / NEXT_PUBLIC_* 环境变量,构建时打进 JS bundle;
  • 移动端/桌面端:key 编译进客户端,反编译就能拿到;
  • Agent 工具配置:把 key 写进 prompt、工具描述、示例代码,模型会把它复述出来;
  • CI 日志:echo $API_KEY 调试完忘了删,日志公开;
  • Git 历史:提交过 .env,后来删了文件但历史还在。

报告里的例子属于第一种的变体:共享 key 硬编码,所有用户共用一个凭证。这种设计下,限流、审计、计费全部失效,因为你分不清是谁在调用。

先扫一遍自己的仓库

图:GitHub Secret Scanning 文档,平台侧的兜底扫描(来源:docs.github.com,截图于 2026-10-19)

图:GitHub Secret Scanning 文档,平台侧的兜底扫描(来源:docs.github.com,截图于 2026-10-19)

我常用两个工具,一个查当前文件,一个查历史:

# 当前工作区
gitleaks detect --source . --no-git

# 连同 git 历史
gitleaks detect --source . --log-opts="--all"

TruffleHog 也可以,它对高熵字符串的检测更激进,误报也多一些。两个都跑,交叉看。

除了工具,我还会手动 grep 几类模式:

grep -rn "sk-[A-Za-z0-9]\{20,\}" src/ --include="*.ts" --include="*.vue"
grep -rn "api[_-]\?key" src/ -i
grep -rn "Bearer " src/

扫完不是删掉就完事。只要 key 进过公开仓库、进过前端 bundle、进过日志,就必须当作已泄露,走轮换流程,而不是“删了就当没发生”。

正确的密钥管理长什么样

图:TruffleHog 仓库首页,对高熵字符串更敏感的另一种扫描器(来源:github.com/trufflesecurity/trufflehog,截图于 2026-10-19)

图:TruffleHog 仓库首页,对高熵字符串更敏感的另一种扫描器(来源:github.com/trufflesecurity/trufflehog,截图于 2026-10-19)

我现在的做法是三条:

1. 密钥只存在服务端

前端永远不碰 key。需要调用模型时,前端调自己的后端,后端用服务端环境变量里的 key 去请求。多一层代理,换来的是密钥不出内网。

如果站点是纯静态的(比如这个博客),就不该有“前端调模型”这种需求。真要有,也应该走一个受控的边缘函数,而不是把 key 塞进浏览器。

2. 按用途拆 key,按权限收窄

一个 key 干所有事,是 239 个模型共享密钥的根源。正确做法是:

用途 key 类型 权限
线上聊天 生产 key 只允许 chat 模型,限流
批处理 批处理 key 只允许指定模型,低配额
开发测试 测试 key 低配额,可随时吊销
CI 短期 token 单次任务有效

云厂商大多支持子账号 / 项目级 key,用起来不麻烦,麻烦的是“一开始没设计”。

3. 短期凭证 + 自动轮换

长期 key 的问题是“泄露了也不知道”。我倾向用短期凭证:每次任务签发一个 15 分钟有效的 token,任务结束就失效。这样即使泄露,窗口也很小。

轮换要有自动化:定期生成新 key、灰度切换、旧 key 保留一个过渡期再吊销。手动轮换的结果通常是“永远没轮换”。

泄露之后的处置顺序

如果确认泄露,按这个顺序走:

  1. 先吊销,别先查原因。攻击者不会等你排查完。
  2. 查用量和日志,确认有没有异常调用、有没有数据外泄。
  3. 评估影响面:这个 key 能访问哪些模型、哪些数据、哪些下游服务。
  4. 轮换所有同源凭证(同一个环境里的其他 key 也要换)。
  5. 清理泄露源:前端 bundle、Git 历史(必要时 filter-repo)、日志、截图。
  6. 补上检测:CI 里加 secret scanning,提交前拦截。

第 5 步的 Git 历史清理要谨慎:重写历史会影响所有协作者,先备份、再通知、最后强推。能用 git filter-repo 就别用 filter-branch。

为什么大模型让这件事更严重

传统 API key 泄露,损失通常是“被白嫖调用”。大模型 key 泄露,损失可能是:

  • 账单:模型调用按 token 计费,被刷一晚可能上千美元;
  • 数据:如果 key 绑定了知识库、微调模型,攻击者能读到你的私有数据;
  • 声誉:攻击者用你的 key 生成违规内容,责任算在你头上;
  • 供应链:一个 key 打通多个模型,等于一次泄露影响整条产品线。

报告里那 239 个模型共享一个 key 的设计,等于把上面四条全占了。

前端 bundle 里怎么找 key

如果你怀疑自己的站点泄露了 key,别只搜源码,要搜构建产物:

# 在 dist 里找疑似密钥
grep -rEo "sk-[A-Za-z0-9_-]{20,}" dist/ | head
grep -rEo "(api[_-]?key|token)[\"']?\s*[:=]\s*[\"'][^\"']{16,}" dist/ | head

更彻底的办法是打开浏览器开发者工具,在 Sources 里全局搜索 Bearer、apiKey、sk-。前端能看到的,攻击者都能看到。

找到之后别急着删代码。先确认这个 key 的权限范围,再决定是轮换还是吊销。删代码只是止血,泄露的 key 还在外面。

CI 里加一道 secret scanning

光靠人记不住。CI 里加一步扫描,提交前就拦住:

- name: secret scan
  run: |
    npx gitleaks detect --source . --no-git --exit-code 1

GitHub 自带 secret scanning 也可以开,但它只覆盖已知格式的密钥,自建服务的 token 不一定能识别。两层一起用,误报可以调规则。

扫描规则要定期更新:新的模型厂商、新的 key 前缀,旧规则不一定覆盖。我每季度会看一次误报和漏报,把规则调一遍。

轮换要自动化

手动轮换的结果通常是“永远没轮换”。我的做法是:

  1. 密钥存进 secret manager(Vault、云厂商的 KMS、或者至少是加密的环境变量);
  2. 服务启动时拉取,不落盘;
  3. 设置过期时间,到期前自动生成新版本;
  4. 旧版本保留一个过渡期,确认无调用后吊销。

这套流程初期麻烦,但一旦跑起来,泄露的处置就从“紧急事故”变成“例行操作”。

算一笔账

假设一个 key 能调 239 个模型,平均每次调用 2000 token,单价按每百万 token 几美元算。被刷一晚上(8 小时,每秒 10 次请求),大概是 5.7 亿 token——账单能到几千美元。如果模型里有贵的推理模型,数字还要翻几倍。

这还没算数据泄露和声誉损失。相比之下,花半天时间做密钥管理,投入产出比高得离谱。

常见误区

误区一:“key 只在前端用,后端没有,泄露也没事”。前端 key 就是公开的,任何人打开开发者工具都能拿到。所谓“只在前端用”,等于把 key 印在广告牌上。

误区二:“设了额度上限就安全了”。额度上限能限制损失,但不能阻止泄露。攻击者可以在额度内慢慢用,或者把 key 转卖。额度是止损,不是防御。

误区三:“轮换太麻烦,等出事再说”。轮换麻烦一次,泄露麻烦一年。把轮换做成脚本、接进 CI,麻烦就只剩第一次。

我的判断

密钥管理是个“做了没人夸、不做会出事”的事。大部分团队不是不知道要做,而是“先上线再说”。大模型时代这个拖延的代价变高了,因为 key 背后的资源比传统 API 值钱得多。

最低成本的起步动作:今天花十分钟,跑一遍 gitleaks,把仓库里的 key 扫出来;然后把前端直连的调用挪到服务端。这两步做完,风险能降一大半。

补充一个细节:扫描时别忘了 CI 配置、Dockerfile、示例代码、测试 fixture。这些地方经常藏着“临时用一下”的 key,而且很少被清理。我见过最离谱的一个案例,key 写在 README 的示例里,仓库是公开的,挂了半年没人发现。这类问题的共同点是“没人负责”:写的人觉得是临时,看的人觉得不关我事。把 secret scanning 接进 CI,等于给这件事找了个自动负责人。扫描规则会误报,但误报的代价远低于漏报。漏报一次,可能就是 239 个模型一起裸奔。把这件事当成上线检查的一部分,而不是一次性的运动。

← 返回文章列表

评论

…