一次"不再询问",把数据库密码写进了公网仓库:Claude Code 凭据泄露复盘
先说结论:这次的锅不在"AI 会不会写错命令",而在一个所有人都点过无数次的按钮——“Yes, and don’t ask again”。
起点是一个按钮
Claude Code 执行 Bash 命令之前会征求同意,三个选项里第三个是"Yes, and don’t ask again"。点下去之后,工具会把这条命令原封不动变成一条允许规则,落盘到项目根目录的 .claude/settings.local.json。
如果被批准的是 Bash(npm run:*) 这类模式,存下来完全合理。麻烦在于被批准的若是"一条具体命令",存进去的就是那条完整命令,一字不差,包括夹在里面的密码:
{
"permissions": {
"allow": [
"Bash(PGPASSWORD='ExamplePassword123' psql -h db.example.com -U admin)"
]
}
}
这串东西就躺在仓库里,等着下一次 git add . 把它带走。
Apiiro 数出来的账
11 月 5 日,安全公司 Apiiro 的研究员 Shmulik Cohen 和 Matan Giladi 把 GitHub 上公开的 .claude/settings.local.json 扫了一遍:104,185 份文件,分布在 103,617 个仓库。其中 1,122 份至少含一个真实凭据,合计 1,379 个。再把历史提交、fork,以及同目录下共享的 .claude/settings.json 一并算进来,总数是 2,539 个凭据、1,994 个仓库、63 家组织,以软件公司为主。
| 凭据类型 | 数量 | 为什么扎眼 |
|---|---|---|
| 数据库密码 | 379 | 大量是生产库,含一套 Azure Postgres 生产密码、某医疗记录库的 owner 密码 |
| Supabase service-role key | 122 | 绕过行级安全,等于全表读写权限 |
| SSH 登录 | 43 | 部分直接是 root |
| GitHub / GitLab / npm token | 42 | 能推恶意包,也能拉私有仓库 |
| AWS 密钥 | 23 | 含某数据库厂商自己的管理员级云密钥 |
| 其他(Square token、私钥、包仓库 token 等) | 约 1,930 | 兜底那一类 |
被点名的例子里还有一枚 Square 生产 token。63 家受影响组织里,有一家回信确认:扫描当时,那个密钥仍然有效。
为什么偏偏是它
同一次研究里,Apiiro 横向比了 16 款编码代理的权限实现。其他家要么把批准记录放在仓库之外,要么只存脱敏后的命令模式,要么压根没有"持久免打扰并写入项目文件"这个设计。只有 Claude Code 默认把整条命令原文写进工作树。
它也不是完全没做防护:第一次生成这个文件时,会往当前这台机器的全局 git excludes 里加一条。可这行保护不跟着仓库走,换个目录 clone 就失效了,更管不到在这个机制出现之前就已经提交上去的旧版本。
更值得琢磨的是另一半:Apiiro 能追溯的 1,105 个仓库里,有 539 个(接近一半)是这个代理自己提交的——提交作者行里带着 Claude 的共同署名。也就是说,它跑完命令、拿到批准、写好配置文件,然后顺手把配置文件一起提交了,人压根没注意。
扫描器为什么没拦住
Apiiro 把 1,122 份文件喂给了两个最常用的开源密钥扫描器:
- Gitleaks 命中 564 份,漏掉 558 份;
- TruffleHog 命中 643 份,漏掉 479 份;
- 两个都没报的:215 份。
漏掉的基本是同一类写法:PGPASSWORD=…、sshpass -p …、mysql -u root -p…。这类密码没有 sk- 那种固定前缀,也没有云厂商密钥的固定结构,写不出正则,工具就沉默放行。何况它们在 shell 脚本里本来就是合法写法,误报成本高,扫描器宁愿不报。
而这些文件在默认分支上,最长的一次挂了 217 天。
这事其实早就有人提过
2025 年 8 月,anthropics/claude-code 的 issue #5544 就写着:带凭据的命令被以明文原样存储。2026 年 1 月 3 日,一个机器人以 “not planned” 关掉了它,理由是 60 天没有活动。截至 11 月 7 日,公开渠道里没看到 Anthropic 对这份研究的正式回应。
今天就能做的五件事
顺序很重要:先止血,再清理。
第一,先轮换,再清历史。删文件、改历史都不会让已经流出去的密码失效。AWS 密钥先去 IAM 禁用再重建,数据库密码先改掉,再谈别的。
第二,把它从索引里摘出来。
git ls-files .claude/settings.local.json
git rm --cached .claude/settings.local.json
printf '\n.claude/settings.local.json\n.claude/settings.json\n' >> .gitignore
git commit -m "chore: stop tracking Claude Code local settings"
第三,查历史,别只盯工作区。当前那份文件只是冰山一角。
git log --all -p -- .claude/ | grep -nE 'PGPASSWORD=|sshpass -p|mysql .*-p'
gitleaks git -v
trufflehog git file://. --only-verified
fork 也要一起看。Apiiro 的数据里,74 个仓库的 211 个 fork 都带着这些凭据的副本。
第四,从写法上断根:传变量,不传值。
export PGPASSWORD="$DB_PASSWORD"
psql -h db.example.com -U admin
这样被批准并存下来的是 $DB_PASSWORD,不是密码本身。生产环境再进一步,用密钥管理服务,只把引用传进去。
第五,把代理的配置目录纳入扫描。.claude/、.cursor/、.gemini/、.specstory/ 都该进 CI 的扫描清单和 pre-commit 钩子,而且必须扫 git 历史,不能只看当前工作区。
这不是某一家的事
任何把"用户批准过的操作"连同参数一起持久化进项目目录的工具,都有同样的形状,区别只在文件放在哪、存原文还是存模式。挑工具的时候,这算一个该看的指标。
说到底,这件事别扭的地方是它利用了一个很人性的东西:没人愿意在同一条命令上被问三次。安全设计如果只在"多问一句"和"少问一句"之间做取舍,最后一定有人拿便利换安全——而且是用一次点击,换掉一串生产密码。
先把 .gitignore 补上吧,五分钟的事。
参考资料:Apiiro 研究博客《Coding Agents Copy Your Secrets to a Hidden File》(2026-11-05);The Hacker News、CybersecurityNews 相关报道(2026-11-07);GitHub issue anthropics/claude-code #5544。
- 点赞
- 收藏
- 关注作者
评论(0)