1,122 个公开仓库里,躺着真密码

1,122 个公开仓库里,躺着真密码

先问一句:你在 Claude Code 里点过“Yes, and don't ask again”吗?

点过的话,它会把那条命令原样存进项目里的 .claude/settings.local.json。命令里如果带着数据库密码、云厂商密钥、API token,也一并存进去。

这个文件本来该留在你的电脑上。但 Apiiro 的研究人员扫了一遍 GitHub:公开可见的 .claude/settings.local.json 有 104,185 份,分布在 103,617 个仓库里。其中 1,122 份带着真实密钥,占 1.1%。

把这些文件、git 历史、团队共享的 settings.json、仓库里的 agent 对话记录全算上,一共 2,539 个真实密钥,散在 1,994 个公开仓库里,涉及 63 家组织。

密码是怎么进去的

流程很日常。

你让 Claude Code 连一下 staging 数据库,或者跑个迁移。它生成一条命令,命令里带着配置文件里的连接串,因为不带就跑不起来。它照例问你:要执行吗?

你选了“Yes, and don't ask again”。

Claude Code 把这条命令写进 .claude/settings.local.json 的 allow 列表,下次不再问。这是 Claude Code 自己的行为,不是模型的行为——Apiiro 用 Opus 5.5 和 Fable 5.1 都复现了。

问题出在文件的归属上。Claude Code 创建这个文件时,会把它加进你的全局 gitignore,覆盖常见情况。但它不会写进仓库自己的 .gitignore。

被批准过的命令,连同密码一起落进了项目文件所以这几种场景全都漏了:文件夹后来才变成 git 仓库、在另一台机器上 clone、文件已经被提交过。

更常见的是 Claude 自己把它提交了。在有真实密钥的 1,105 个仓库里,539 个的提交记录里有 Claude 作为 co-author 的 commit 改过这个文件。从 Sonnet 4.5 到 Opus 5.5,都出现过。

里面都是什么

Apiiro 列了几个能对上号的案例:

  • 一家数据库厂商:它自己的云 API key,带 manage 权限、没有过期时间,外加集群地址
  • 一家 B2B 创业公司:生产环境 Azure Postgres 密码
  • 一家软件外包公司:Square 生产环境访问 token
  • 一个 1,000+ 星的知名开源项目:OpenAI 格式的 API key
  • 一家做医疗记录应用的 IT 服务商:云 Postgres 数据库的 owner 密码
  • 一家支付创业公司:postgres 角色的密码

还有 43 条 SSH 登录凭证,其中一些是 sshpass 命令里的 root 密码。有大学的仓库永久批准了一条连公开服务器的 root SSH 命令。

按类型分:379 个数据库密码、122 个 Supabase service-role key、103 个 Supabase 账号 token(能控制账号下所有项目)、42 个 GitHub/GitLab/npm token、23 个 AWS key、6 个私钥。

这些文件在公开分支上躺了很久。中位数是 217 天,118 个仓库超过一年。74 个仓库的 211 个 fork 带着同样的密钥。就算后来删了文件也没用——历史版本里还有 734 个密钥,分布在 509 个仓库,当前副本里已经找不到。

扫描器也漏

Apiiro 用 gitleaks 和 TruffleHog 跑了这 104,185 份文件。1,122 份有密钥的文件里,gitleaks 报出 564 份,TruffleHog 报出 643 份,215 份两个都没报。

漏的原因是格式。扫描器擅长找“厂商前缀 + 一长串随机字符”的 key;但 PGPASSWORD=xxx、sshpass -p xxx、mysql -p xxx、curl 里的 bearer header,都是普通命令行里的普通单词。密码没有格式,扫描器就不认识。

这个缺口不只影响这一个文件。agent 的对话记录、memory 文件、handoff 笔记,只要它记录了你跑过的命令,同样的密码就会以同样的形式躺进去。

现在该做什么

如果你用 Claude Code,按这个顺序查:

  1. 在 GitHub 组织里搜 .claude/settings.local.json 和 .claude/settings.json。能搜到的,就是现在任何人都能读的。
  2. 查 git 历史。文件今天不在,不代表没提交过;提交过,每个 fork 和 clone 都有。
  3. 打开本机所有这个文件,逐条读 allow 列表。每一条都是你批准过的命令,里面可能带着密码、 token、连接串。
  4. 发现密钥,先轮换,再清理。删文件不解决历史、fork 和 clone。

以后别再往命令里塞明文密码。用 $DB_URL 这类变量引用密钥管理器里的值,allow 列表记录的就是 $DB_URL,不是密码本身。批准命令时用模式匹配,比如 Bash(npm run:*),别批准整条字面命令。

团队层面,把 .claude/、.cursor/、.gemini/、agent memory 文件当成 .env 一样对待:加进 pre-commit 和 CI 扫描,并且确认扫描器能抓命令里的密码,不只认 key 格式。

最后

这件事最值得注意的地方,不是“AI 又泄露了密码”。

是权限设计:一个为了少弹几次确认的便利功能,把“你批准过的完整命令”当成了可以落盘、可以进 git、可以跟着项目走的配置。它默认你会在个人机器上用,默认这个文件不会离开项目目录,默认全局 gitignore 能兜住。

三个默认,在真实工程里都不成立。

密码不是模型偷的,是我们自己一条条批准、一次次提交上去的。


相关链接:

评论

暂无评论。

登录后可发表评论。