Py学习  »  Git

审批疲劳:Claude Code 把 2539 个密钥摊在了公开 GitHub

大模型安全研究 • 4 小时前 • 13 次点击  

一个再普通不过的点击,把生产数据库的密码送上了公开仓库。

让编码智能体查一下预发布环境为什么登录不上,它给出的命令里带着数据库密码,因为不带就连不上。它照规矩问一句要不要执行。同一场会话里这样的询问冒出来十次,到第十次,多数人已经不逐字读那行命令了,点「是,而且别再问了」。省下的是几秒钟,留下的是一条明文记录。

安全公司 Apiiro 把这条记录的去向查了个底朝天,11 月 5 日发布报告,11 月 8 日补上 Anthropic 方面的回应。Ars Technica 在 11 月 9 日跟进报道后,这件事才从开发者圈子里的小范围提醒变成公开讨论。结论不复杂:不是模型把密码说漏了嘴,是工具把用户批准过的命令原封不动存进了项目文件,而这个文件跟着代码一起被提交了。

一句话看懂这份报告:

●泄露的载体是一份「允许清单」。用户点选「别再问了」之后,整条终端命令作为一条允许规则(allowlist)写进项目目录里的 .claude/settings.local.json,命令里的密码、令牌、私钥一并落盘。

●规模是十万量级。公开 GitHub 上共有 104185 份这份本地设置文件,分布在 103617 个仓库;其中 1122 份(1.1%)含真实密钥。

●把历史版本、共享设置、智能体对话记录都算上,是 2539 个不同的真实密钥、散落在 1994 个仓库里,波及 63 家组织,一家已确认自己的密钥当时还有效。

●两道现成防线都漏。Claude Code 只把文件加进开发者本机的全局忽略规则,从不写进仓库自己的忽略规则;gitleaks 与 TruffleHog 两款常用密钥扫描器合起来,仍有 215 份含密文件一份都没标出来。

●删文件解决不了。734 个密钥只存在于历史版本里,211 个派生仓库抄走了同样的密钥,凭证只能逐个轮换。

一、「别再问了」到底往项目里写了什么

开发者本机本来就存着密钥:配置文件里的数据库连接串、本地云工具里的密钥、.env 里的接口令牌。让 Claude Code 去查预发布环境登录失败,或者跑一次数据库迁移,它给出的命令必须带上这些值,否则任务根本跑不通。

Claude Code 执行终端命令前会询问,一次会话里问很多次。Apiiro 的判断很直白:问到第十次,多数人已经不读命令了,而密码只是长长一行里的一个词。此时点「是,而且别再问了」,工具会把这条命令作为一条允许规则追加到项目目录里的本地设置文件,规则的内容就是命令字符串本身。

「别再问了」把整条命令写进项目设置文件,安全规则与泄露规则的对比,图片来自 Apiiro 报告

图里把两种写法摆在一起看得很清楚:写成 Bash(npm run:*) 这样的模式,规则里不会出现任何密钥;写成 Bash(sshpass -p 密码 ssh root@主机) 这样的字面命令,超级管理员(root)密码就永久留在了文件里。同样的还有 curl 命令里的授权请求头,一整串令牌跟着规则一起被记下。

这里要划一条界线:这是 Claude Code 自身的产品行为,不是大模型偶尔犯糊涂。 Apiiro 用 Anthropic 最新的 Opus 5.5 和 Fable 5.1 都复现了,换了模型照样发生。研究者还比对了 16 个编码智能体存放授权记录的方式,Claude Code 是唯一一个被确认默认把完整的批准命令写进项目文件的。

Anthropic 在 Claude Code 官方设置文档里写明了这个文件的定位:它属于「项目本地」层级,只影响当前这一个项目,Claude Code 第一次写入时会把  **/.claude/settings.local.json 加进开发者本机的全局忽略规则,让文件不进入提交。文档还特意补了一句,如果是使用者手动创建的文件,就得自己把它加进 .gitignore。

问题恰恰出在这句补充上。全局忽略规则只在这台机器上生效,既不会随项目走,也管不到三种常见情形:一个原本不是仓库的文件夹后来被初始化成仓库、项目被克隆到另一台机器、以及这个文件早就被提交过。规则从不进入仓库自己的 .gitignore,所以项目传到谁手里,都没有任何东西拦着这份文件被提交。

更隐蔽的一层是:Claude Code 其实知道文件已经被版本控制系统跟踪了。其权限文档说明,被跟踪的副本会被当作「仓库提供」的设置,调试日志里会记下「已被索引跟踪,忽略规则对已跟踪文件不适用」。但使用者不会收到任何提示,下一次批准照旧追加进那个已经被跟踪的文件。

还有更直接的。Apiiro 在追踪到的 1105 个含真实密钥的仓库里发现,539 个仓库有由 Claude 共同署名的提交改动过这份文件,署名覆盖从 Sonnet 4.5 到 Opus 5.5 的多个型号。Claude Code 的提交指令里明确要求模型不要提交 .env 或 credentials.json,唯独没有把自家这份设置文件列进禁提交名单。工具自己动笔,把自己写下的密钥提交了。

二、104185 份文件里,躺着什么

Apiiro 的采集口径是全量:2026 年 11 月 1 日至 3 日,对默认分支做只读的代码与提交搜索,把公开 GitHub 上能找到的 104185 份本地设置文件全部下载下来逐一检查。

指标
数值
口径
公开本地设置文件
104185 份
分布在 103617 个仓库
含真实密钥的文件
1122 份,占 1.1%
按厂商格式或紧邻公开主机判定
这些文件里的不同密钥
1379 个
636 个厂商格式密钥 + 743 个公开主机密码或令牌
加上历史、共享设置、对话记录
2539 个不同密钥
分布在 1994 个仓库
受影响组织
63 家
一家已确认密钥当时有效
密钥扫描器漏标文件
215 份
两款工具都没标出

那 1379 个密钥按类型拆开,超过三分之一直接通向数据库:379 个数据库密码、122 个 Supabase 服务角色密钥,另有 103 个 Supabase 账户令牌,后者的权限覆盖账户下的每一个项目。剩下的是第三方服务接口令牌、大模型接口密钥,加上 43 个服务器登录凭据、42 个 GitHub、GitLab 与 npm 令牌、23 个 AWS 密钥和 6 个私钥。Apiiro 在图上把能直接打开数据库的 501 个单独标成了绿色。

公开设置文件里 1379 个真实密钥按类型分布,绿色为可直接打开数据库的 501 个,图片来自 Apiiro 报告

落到具体组织,名单比数字更刺眼。

组织类型
暴露了什么
某数据库厂商
自家云接口的管理级密钥,无过期声明,还带着集群主机地址
一家企业服务初创公司
生产环境 Azure Postgres 数据库密码
一家软件外包公司
Square 生产环境访问令牌
一个超过千星的开源项目
OpenAI 格式的接口密钥
另一个开源项目
npm 发布令牌
医疗记录应用的 IT 服务商
该应用云端数据库的属主密码
一家支付初创公司
数据库超级角色的密码

这些组织从学生小组、黑客松队伍到初创公司、软件外包商和成熟厂商都有,好几家是通过自己的演示仓库和实训仓库漏出去的。允许清单里还留着 43 个公开服务器的登录凭据,部分是 sshpass 命令里的超级管理员密码;某所大学的仓库把一条通往公开服务器的超级管理员远程登录命令设成了永久批准。

没有人打算公开一个密钥。密钥在命令里,命令被批准,文件跟其他代码一起提交了。 Ars Technica 的报道把数字概括为「超过十万份文件、约 1100 份来自 63 家组织含密钥」,与 Apiiro 原文的 1122 份是同一口径,只是取了整数。

三、删掉文件也没用:历史、派生仓库和别的文件

Apiiro 特意查了这些文件的存活时间。到 2026 年 11 月 2 日,每一份含密文件都还躺在各自仓库的默认分支上,没有一份被清掉。含真实密钥的仓库里,这份文件首次被提交的中位时间是 217 天前,118 个仓库超过一年。这不是一次刚发生的事故,是一年多里慢慢攒出来的一个公开密钥库。

删文件这一招之所以不够,有三层原因:

●历史版本还在。734 个真实密钥只出现在旧版本里、当前的副本中已经看不到,分布在 509 个仓库,多数正是因为文件被删了。版本控制系统的历史不会被一次删除抹掉。

●派生仓库照抄。74 个仓库的 211 个派生仓库带着同样的密钥。原仓库修好了,分叉出去的那些仍在。

●别的文件也在装。Apiiro 顺手查了其他几类智能体产物,结果如下。

文件类型
检查数量
含真实密钥
团队共享的 .claude/settings.json
76011 份
298 份
智能体对话记录(.specstory、aider、Cursor 导出)
2523 份
120 份,分布在 87 个仓库
智能体记忆文件(MEMORY.md 一类)
1200 份
9 个仓库
交接与部署笔记
2095 份
33 份
VS Code Copilot 自动批准设置
173 份
9 个仓库

共享设置文件是团队主动提交给同伴用的,里面的密钥大多同样以完整命令行形式待在允许规则里;带密钥的交接笔记里,有三分之一是智能体自己写的。把上面这些和文件历史一起计数,才是 2539 个不同密钥、1994 个仓库的总数。

范围也不止 GitHub。Apiiro 查了 31053 个 Hugging Face Spaces,其中 126 个带着这份本地设置文件,有一个的允许规则里写着数据库服务密钥和用户密码;GitLab.com 上抽查的 69 份公开副本里,2 份含真实密钥,包括一个 GitLab 访问令牌。

还有一条规律值得记:文件越大越危险。允许清单是攒出来的,点「别再问了」的次数越多,里面存的完整命令越多,攒进密钥的概率就越高。

设置文件体积越大、含真实密钥的比例越高,超过 8 KB 的文件接近一成中招,图片来自 Apiiro 报告

小于 500 字节的文件只有 0.17% 含真实密钥,超过 8 KB 的升到 9.9%,相差近六十倍。授权疲劳不是抽象的用户体验问题,它有一个可以量化的安全代价。

四、两款密钥扫描器,为什么漏掉 215 例

既然密钥明文躺在公开仓库,常见的自动扫描为什么没拦住?Apiiro 把 gitleaks 和 TruffleHog 跑遍了全部 104185 份文件:在 1122 份含真实密钥的文件里,gitleaks 标出 564 份,TruffleHog 标出 643 份,215 份两款工具都没报。

缺口集中在「命令里的密码」。扫描器的拿手好戏是识别厂商格式:一个固定前缀,加一串长随机数。可写在 PGPASSWORD=、sshpass -p、mysql -p 后面的密码,或者 curl 命令授权请求头里的令牌,在扫描器眼里只是一行普通命令里的普通单词,没有可匹配的特征。

一条两款密钥扫描器都没标出的真实规则,图片来自 Apiiro 报告

图里那条真实规则里,数据库密码就紧挨着一个公开主机地址,两款工具都判定「无发现」。Apiiro 的提醒比这个案例本身更重要:同样的「命令里带密码」写法会出现在智能体记录自己干过什么的所有地方,对话记录、记忆文件、交接笔记,按厂商格式调优的扫描器在这些地方一样会漏。

Ars Technica 的报道也引了这个数字,并提到 Claude Code 团队正考虑把密钥扫描内建进工具里。只是从上面的数据看,内建扫描若仍只认厂商格式,这 215 例还是拦不住。

五、留在本地也不安全:恶意软件已经在找这份文件

多数这类文件根本没上 GitHub,它们留在开发者本机。Apiiro 的判断是,留在本机不等于安全:允许清单是固定路径下的一份纯 JSON 文件,格式统一,里面的密钥大多紧挨着它们能打开的主机地址。对恶意软件来说,这是一份现成的清单,写清楚了开发者能碰到哪些系统、以及打开它们的密码,一次文件搜索就能拿到。

已经有人这么干了。2025 年 9 月的 Shai-Hulud 蠕虫在感染 npm 包后,会下载 TruffleHog 扫受害者的磁盘,而这款工具能标出 1122 份含密文件中的 643 份。往前三周,被投毒的 Nx 软件包直接指使受害者本机的 Claude Code 和 Gemini CLI 去磁盘上搜密钥。文件也不必经过版本控制系统才能外流:发布出去的 npm 包、构建镜像时拷进去的文件、压缩包、同步文件夹,都带得走它。加上一条,批准过一次的密钥会一直留在文件里,直到有人手动删掉那条规则,哪怕那条命令早跑完了。

六、一年多前的四个问题单,都被静默关掉了

这不是第一次有人报告。Apiiro 在「此前的工作」里列得很清楚:从 2025 年 8 月起,Claude Code 的使用者就在 GitHub 问题单 5544、11982、13106、91270 里陆续描述过这个机制,Anthropic 一条都没有回复,其中三个被系统按「不活跃」或「重复」自动关闭,Ars Technica 的说法是搁置 60 天后关闭。ironPeak 在 2026 年 2 月于自家仓库发现同一份文件,抽样 100 个带智能体配置目录的仓库,测出 2.4% 的历史里带着密钥;Lakera 4 月在 30 个 npm 包的 33 份这类文件里找到凭据。

Apiiro 这次补上的,是全量计数、组织层面的案例、扫描器的盲区,以及其他同样会装密钥的智能体文件。他们在 11 月 2 日和 3 日逐个通知了受影响的组织,也通过 Anthropic 指定的披露渠道做了通报。

Anthropic 的公开回应来自 Claude Code 负责人 Boris Cherny。11 月 7 日他在 Hacker News 的讨论串里露面,整个周末发了约十条评论,原话是「以往 Claude Code 没有做过密钥清理(市面上有不少好工具),但我们在研究能做什么来防止这件事」。他给出的方向有几条:让自动模式(auto mode)覆盖这种情况、内建基础密钥扫描,先做几个原型看清取舍再上线。他同时认为这份文件本就不该进版本控制,工具创建时会自动忽略,暴露出来的那些是手动创建并签入的,并建议改用自动模式、开启平台提供的密钥扫描。

有读者当场指出,自动模式碰到带敏感命令的询问时,一样会把命令存进设置文件。Cherny 回了句「好眼力(good catch)」,说团队正在想怎么改进,欢迎提想法。

讨论串里另一段交锋更值得安全从业者看。有人要求给智能体画确定性的边界,明确说「我不想要一套概率式的安全」,理由是只要智能体能自己决定做某件事,任何能把文本注入它上下文的攻击者也能让它做同一件事;也有人反驳,允许清单本身并不是确定性的,因为智能体几乎每次都会写一段脚本然后请求执行,而没有开发者真会去逐行审那段代码。有开发者把话说得更透:确定性边界大概得靠沙箱、微虚拟机、独立身份这类系统层的东西,概率式护栏只能叠在上面。 这段争论和这份报告说的是同一件事:把安全寄托在「人会在弹窗前认真读一遍」上,从来就不牢靠。

七、现在该做什么

Apiiro 给了两组可直接照做的清单,这里按原意搬过来。

开发者侧五条:

1. 如果文件已被版本控制系统跟踪,用 git rm --cached .claude/settings.local.json 取消跟踪,并把它写进仓库自己的 .gitignore。

2. 轮换这份文件曾经装过的每一个凭据。删文件清不掉提交历史、派生仓库和克隆。

3. 就算从没提交过,也打开本机这份文件,删掉所有含密钥的规则。

4. 别把密钥直接敲进命令,改用 $VAR 从密钥管理服务引用,让规则记下的是 $DB_URL 这样的变量名,而不是明文。

5. 批准模式而不是字面命令,例如 Bash(npm run:*)。

安全团队侧四条:

1. 把 .claude/、.cursor/、.gemini/、.specstory/、智能体记忆文件当成和 .env 同级的密钥承载目录,笔记本和仓库一视同仁。

2. 纳入提交前检查与持续集成的密钥扫描,连历史一起扫;确认扫描器能抓到命令里的密码(PGPASSWORD=、sshpass -p、mysql -p、授权请求头),而不只是厂商格式。

3. 把 .claude/settings.local.json 加进组织的忽略规则模板。

4. 优先处置那些和公开主机地址配对的凭据,它们才真的能打开门。

Apiiro 还给了一个三步自查:在组织的 GitHub 里搜这两个设置文件名,每一条命中都是当下任何人可读的;翻每个仓库的历史,文件今天不在不代表以前没在,有历史就有派生和克隆;把本机每一份允许清单打开读一遍,逐条找密码、令牌、密钥、连接串。发现之后,顺序是先轮换,再清理。

八、问题不在那个点击,在「批准一次就永久明文」

把这件事放回最近一个月的脉络里看,揭示的是同一个结构性变化:智能体一旦被授以执行权,它写下的授权记录、设置文件、对话记录、交接笔记,全都成了新的凭据容器。

而围绕代码的这套防线,是按「人自己写命令、自己决定提交什么」的时代设计的。忽略规则假定创建文件的人知道要顺手加一条规则;密钥扫描器假定密钥长得像厂商发下来的那一串;权限弹窗假定屏幕前的人每次都会认真读完。三个假定在智能体接手之后同时失效:工具自己创建文件、自己写完整命令、自己提交,人只在最开始点了一下「别再问了」。

Apiiro 在报告里给的那句判断最值得留下来:出问题的习惯到处都一样,把密钥敲进一条命令,然后让智能体把它记下来。只要这个习惯还在,换一个文件名、换一个智能体,同样的十万份文件还会再攒一遍。当工具开始替人做决定、替人写命令、替人提交代码,安全边界的粒度就得跟着细到每一条被批准的命令里,而不是停在「用户同意过」这四个字上。

参考链接

●Apiiro 研究报告《Coding Agents Are Leaking Your Secrets to GitHub》(apiiro.com):https://apiiro.com/blog/coding-agents-leaking-secrets-github-dont-ask-again

●Ars Technica 报道《Claude Code is accidentally exposing secrets all over GitHub》(arstechnica.com):https://arstechnica.com/ai/2026/11/claude-code-settings-files-expose-thousands-of-secrets-on-github/

●Anthropic Claude Code 官方设置文档(code.claude.com):https://code.claude.com/docs/en/settings

●Hacker News 讨论串,含 Boris Cherny 回应(news.ycombinator.com):https://news.ycombinator.com/item?id=50407882

●Claude Code 问题单 5544,2025 年 8 月首次报告该机制(github.com):https://github.com/anthropics/claude-code/issues/5544

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/203295