WheatField
WheatField

如何从 Git 历史中删除敏感信息

September 11, 20241379 words, 7 min read
Authors

这两天在开发一个通过 LLM 一键总结网页的 chrome 插件,测试过程中在本地添加了 API token,但忘记了把配置文件添加到 .gitignore,开发了一段后回头查看提交记录时,才发现 token 已经被 commit 到历史记录中了。

在与 Claude Sonnet 进行一番探讨沟通之后,决定通过 git-filter-repo 来清理敏感信息,经过一番折腾并在 AI 的耐心指导下后终于成功的清除了 token,在此作个记录,以备不时之需。

#定位敏感信息

第一步是定位敏感信息所在的所有历史提交,这一步不是必须的,但可以辅助确认有哪些文件包含敏感信息,方便下一步编写替换规则,避免误伤。

操作可以通过 git rev-list --all + git grep 命令完成。

  • git rev-list --all 这个命令会列出所有的 commit hash
  • 然后再结合 git grep 命令去过滤查找敏感信息。

假定 token=sk-a1b2c3d4 是不幸泄露的敏感信息,对应的定位命令就是:

git rev-list --all | xargs git grep 'sk-a1b2c3d4'

或者:

git grep "sk-a1b2c3d4" $(git rev-list --all)

那么,所有包含 sk-a1b2c3d4 的 commit hash 都会被列出:

git grep "sk-a1b2c3d4" $(git rev-list --all)
2b1e4c406dfcf7dc8c892e9b87fbac7bb439d5f9:README.md:+token=sk-a1b2c3d4
26a64b6c8a79531c6ca9c848c650bc92b028d6d2:x.py:+token=sk-a1b2c3d4、
...

从这里大概可以看到一个概要,里面有包含敏感信息的 commit 及涉及文件。如果敏感信息是最近提交的,就比较好办了,先 git reset 到前一个 commit,然后重新提交。或者用 git rebase -i 命令,把包含敏感信息的 commit 之后的所有 commit 都 rebase 掉,然后重新编辑文件,删除敏感信息,重新提交。

但如果敏感信息是很早之前 commit 的,那就需要 git filter-repo 的文本替换功能。

#替换敏感信息

git-filter-repo 支持从文本文件中读取替换规则(通过 --replace-text 选项),并遍历所有历史提交,检查每个 blob 中的内容,然后根据指定的规则进行文本替换,类似于使用 sed 命令替换文本。

这些规则可以是正则表达式,也可以是简单的文本匹配。使用时务必谨慎,建议先用 git grep 确认替换范围,并尽可能使用最小粒度的替换规则。例如,如果泄露的 token 是 sk-a1b2c3d4,那就用 sk-a1b2c3d4==>xxxxxxxx 这样的规则,而不是直接用 sk-* 替换 token,因为这样会把所有包含 sk- 的行都替换掉,即使这些行并不包含敏感信息(比如 sk-II 这样的无关文本)。

初次使用需要安装 git-filter-repo

pip install git-filter-repo

下一步,创建替换规则文件 replace.txt 并执行替换:

echo "sk-a1b2c3d4==>xxxxxxxx" > replace.txt
git filter-repo --replace-text replace.txt

然后再查看历史:

git grep "sk-a1b2c3d4" $(git rev-list --all)

这时应该没有关于 sk-a1b2c3d4 的任何结果了,如果仍然能看到结果,需要检查替换规则是否正确。 最后再检查下替换后的文本,可以看到文本中所有的 sk-a1b2c3d4 都已经被替换成 xxxxxxxx 了。

git grep "xxxxxxxx" $(git rev-list --all)
2b1e4c406dfcf7dc8c892e9b87fbac7bb439d5f9:README.md:+token=xxxxxxxx
26a64b6c8a79531c6ca9c848c650bc92b028d6d2:x.py:+token=xxxxxxxx

##小坑

替换这一步有一个小坑需要注意,如果直接通过编辑器(比如 VSCode)编辑规则文件,那最终保存时文件可能不是以 UTF-8 编码存储的,这样会导致操作无效。笔者在这个地方卡了很久,一遍又一遍的改替换规则,正则表达式,但执行之后都没有效果。

如果是 Linux/Unix 系统,保险的做法是直接通过 echo 命令输出文本规则,然后重定向到文件。

echo "sk-a1b2c3d4==>xxxxxxxx" > replace.txt

#慎用 git-filter-repo

git-filter-repo 通过直接操作 Git 仓库的对象数据库(如 commits、trees 和 blobs)来实现历史记录的重写,除了替换文本,还可以做的事情很多,比如:

  • 删除敏感信息、特定文件
  • 重命名文件目录
  • 修改提交信息,包括作者、日期、内容等

尽管 git-filter-repo 功能强大,但还是要慎用。原则上 commit 历史应该反映真实的开发历程,特别是公共仓库。已经被推送到远程仓库的 commits,它的内容就不应该变动。因为一旦变动,就会破坏 commit 历史的一致性,其他开发者也必需重新同步所有 commits,这相当于篡改了历史并修正了集体记忆,不仅不体面也不道德,这也是为什么有些开发者甚至连 rebase 都反对使用的原因之一。

Commit 是约束,是承诺,是契约,改掉了就相当于破坏了契约精神,破坏了大家都约定俗成的理念。

当然,如果是本地 commit,还没有推送到远程仓库,那就可以自由修改,因为还没有同步给其他协作者。可以 rebase,可以创建新分支,也可以使用 git-filter-repo

#小结

对于个人项目,清除敏感信息后,重新提交,然后推送到远程仓库即可。但如果是团队项目,还是着重防范于未然,规范流程,避免敏感信息泄露。

如果敏感信息已经被推送到远程,最妥善的处理办法还是假定信息已经泄露,立即重置所有相关令牌或密钥,因为代码一旦推送出去,它就不再属于你了,你永远不知道它被谁看到过。