为什么 GitHub 提交没你想得那么私密
译者 | 涂承烨
审校 | 重楼
开发者中普遍存在一种误解,认为一旦删除了提交(commit),它就永远消失了。你可以强制推送(force-push)来重写历史,或者删除包含敏感信息的分支(branch),并以为它已被安全擦除。但 GitHub 和 Git 本身并不这样工作。
事实上,GitHub 能够以不显而易见的方式保留并暴露“已删除”的提交。在某些条件下,你认为已被移除的提交仍然可以被公开访问。这造成了一种隐私假象—开发者感觉安全了,但实际上,敏感信息的痕迹可能仍然可以访问。
在本文中,我将逐步阐述这种情况是如何发生的,演示已删除或私有的提交在哪些情况下仍然可以被访问,并解释这对注重安全的团队、开源维护者以及错误应用 GitHub hygiene的开发者意味着什么。
Git 如何处理“已删除”的提交
Git 是一个分布式版本控制系统,用于跟踪文件的版本。这意味着开发者可以独立拥有自己的分支版本。
其核心在于,Git 是一个内容可寻址的数据库。提交(commits)、树(trees)和 blob 对象(blobs)根据它们的 SHA-1 或 SHA-256 哈希值存储。Git 并非真正“删除”内容—它只是取消对它们的引用。
让我们看看这在实践中是什么样子:
首先,初始化一个新的 Git 仓库并添加一个 README.md 文件。
向 README.md 添加一些更改并再次提交。
此时,我们有两个提交。你可以用 git log 查看它们:
HEAD 是一个位于仓库内 .git/HEAD 文件中的指针。这个文件通常包含对当前分支的引用(例如,ref: refs/heads/main),或者如果你处于分离的 HEAD(detached HEAD)状态,则包含一个特定的提交哈希值。
让我们使用 git reset HEAD^ --hard 重置(reset)到第一个提交。
我们可以看到 HEAD 已切换到第一个提交:
第二个提交消失了。这类似于你进行强制推送(git push -f)时发生的情况。它可能看起来代码已被擦除—但实际上,你仍然可以使用 git reflog 恢复它。
在这里,我们可以看到第二个提交的 SHA-1 哈希值是 2b9714e。
让我们通过重置HEAD将它恢复。
让我们看看提交哈希值。Git 同时支持哈希值的完整和缩短版本:
完整哈希值 (Full hash) | 缩短版本 (Short version) |
fe8b8e6d36d640a29dc893ecc81bc1a2eeead1ed | f38b8e6 |
2b9714ec5b229700eed2ce2dc673b8d8b52a1f | 2b9714e |
Git 中的每个提交都有一个唯一的哈希值作为其“ID”。该哈希值是根据整个提交内容计算出来的,包括:
- 文件和目录结构(“树”对象)
- 提交信息
- 元数据,如作者、提交者和时间戳
- 父提交的哈希值
Git 默认使用 SHA-1(或可选地使用实验性功能 SHA-256)。你通常不需要完整的哈希值—Git 允许你使用缩短版本(通常 4-7 个字符就足够了)。在我的例子中,是 f38b 和 2b97。
这就是 Git 本地工作的方式。
但是 GitHub 呢?这就是事情变得有趣的地方。
GitHub 呢?
GitHub 作为一个构建在 Git 之上的分布式平台,不仅继承了 Git 的去中心化机制,还引入了其自身的复杂性和风险层。
让我们重新审视之前的实验。
我创建了一个公共仓库并添加了两个提交:
然后我运行了:
我们在 GitHub 上看到了什么?
第二个提交消失了—从历史记录中擦除了。
当然,我可以使用本地 Git 工具恢复它(如前所示),但我们还能在 GitHub 本身上访问它吗?
是的!
你可以直接在浏览器中使用以下方式访问该提交:
对于我的例子:
https://github.com/C4tWithShell/demo/commit/cbc61bd83a87561c101a325b03ec9873a7c0cc62
GitHub 警告:
“此提交不属于此仓库的任何分支,可能属于仓库外部的某个分支(fork)。”
但整个提交内容仍然可用。
公共仓库(Public repositories)
由于 GitHub 是一个分布式平台,我们可以将这个想法扩展到连接的仓库—上游(upstreams)仓库及其分支(forks)。
我能访问已删除分支(fork)中的提交吗?
我分叉(fork)了我的演示仓库,在上面工作,并错误地添加了一个新的 .md 文件。
然后我意识到了错误,并通过 git push -f 删除了它。
我不再看到那个提交了,但它真的消失了吗?
多亏了 SHA-1 哈希值,我们可以仅用 4-7 个十六进制字符来定位提交。只有 65,536 (16⁴) 种可能的组合,对于现代机器来说,暴力破解简短的 SHA 前缀是微不足道的,并且完全可以自动化。
我仍然可以在我的分支(fork)中找到那个提交。即使该分支后来被删除,我也可以从原始仓库访问它。
如果上游仓库被删除了呢?
好的,让我们反过来想。
假设我向原始仓库提交了一个秘密(secret),然后在任何人分叉(fork)它之前立即删除了它。我安全了吗?
这次,为了确保,我们甚至删除我的上游仓库。
删除我的演示仓库后,我看到不再有“forked”的链接,并且我看不到 SECRET.md 文件了。
这次,我创建了那个分支的一个分支(fork),并使用简短的 SHA 挖掘提交历史。这意味着我仍然可以恢复 SECRET.md 提交!
因为只要还有一个分支(fork)存在,该提交就存在。
这怎么可能?
这是可能的,原因在于 GitHub 中的仓库网络—仓库与其分支(forks)之间的关系网。你可以在以下位置探索它:
它显示:
- 谁fork了该仓库;
- 提交如何在不同分支(forks)间产生分歧;
- 存在于分支(forks)中但不存在于主仓库中的提交。
因此,当有人分叉(fork)一个仓库时,GitHub 会跟踪父子关系。即使分支(forks)被删除或设为私有,只要:
- 该分支曾经是公开的;
- 在分支被删除/设为私有之前提交已被推送。
那么,它们可能仍然在网络图(network graph)中被跟踪。GitHub 存储的提交哈希值,如果你知道 SHA,仍然可以访问,这是一个已知的元数据泄露途径。
它会影响私有仓库吗?
让我们用一个私有仓库试试。
我创建了一个私有仓库并fork了它。该分支保持私有状态,无法将其设为公开。我在分支中添加了一个额外的文件。
即便如此,我仍然可以从原始仓库使用其简短的 SHA 访问这个提交。
至少在这种情况下,可见性是受控的——分支无法公开,访问仅限于协作者(collaborators)。
但真正的问题在这里...
我见过一些公司开源其内部仓库。在这样做之前,他们通常会彻底清理主仓库。
但是分支(forks)呢?
当一个私有仓库变为公开时:
- 原始仓库的分支网络在发布的那一刻被冻结。
- 在私有分支(forks)中发布之前所做的所有提交都可能变得公开可见。
- GitHub 不会警告你这一点。
因此,即使你的主仓库是干净的,你也可能正在暴露先前私有分支中的秘密。
让我们测试一下。我更改了我的原始仓库的可见性。它只有一个干净的提交和一个文件。
我能访问我私有分支(fork)中的那个提交吗?
成功!我们可以看到在原始仓库发布之前在私有分支(fork)中做出的所有提交。为了验证,让我们向我们的私有分支添加一个新的提交:
现在让我们尝试从公共仓库访问它:
为什么会这样?因为一旦仓库变为公开,GitHub 就会分离仓库网络。之后添加到私有分支(fork)的提交就变得隔离了。
这种行为是双向的:
- 发布后私有分支(fork)中的提交从公共仓库是不可见的。
- 该时间点之后公共仓库中的提交从私有分支(fork)是不可见的。
这是一个 Bug 吗?
不,这是 GitHub 的一种设计行为,他们甚至在文档中提到过。不幸的是,没有多少人深入研究它。
阅读下面—GitHub 关于分支(fork)的可见性说明:
当你将仓库的可见性从私有更改为公共时,其每个现有的私有分支(fork)都将变为私有仓库,并失去对上游网络的访问权限。公共仓库的分支始终是公共的。
可以采取什么措施来解决它?
- 始终将 GitHub 视为公开的(Treat GitHub as Public-Always)假设你推送的任何内容最终都可能被暴露。密钥(Secrets)不属于 Git。使用秘密管理器—Vault、AWS Secrets Manager、Doppler、GitHub Secrets 等。
- 在发布前妥善清理(Clean properly before publishing)使用如下工具:
a.TruffleHog
b.deepsecret
c.semgrep Secrets
d.BFG Repo-Cleaner
e.git filter-repo
我推荐:
- 结合使用 deepsecrets 或 semgrep-secrets 与 truffleHog 或 gitleaks。它们基于上下文检测秘密,而不仅仅是熵(entropy)或正则表达式。例如,passwd: hello 可能会被标准工具遗漏,但不会被 deepsecrets 遗漏。但你也应该预料到会有误报,因为它可能被作为示例提及。
- 在 CI 流水线中自动化扫描。
- 联系 GitHub 支持进行移除(Contact GitHub Support for Removals)如果你不小心推送了敏感数据,联系支持部门,GitHub 可以从其后端清除对象,但这并非即时生效或有保证的。
- 如果秘密暴露了,就轮换它们(Rotate secrets if they get exposed)
作为最重要的一步,轮换秘密而不是试图删除它们。一旦秘密暴露,就假设它已泄露并进行更改。不要心存侥幸!
译者介绍
涂承烨,51CTO社区编辑,具有15年以上的开发、项目管理、咨询设计等经验,获得系统架构设计师、信息系统项目管理师、信息系统监理师、PMP,CSPM-2等认证。
原文标题:Why GitHub Commits Aren’t as Private as You Think,作者:Vladimir Shelkovnikov