
譯者 | 涂承燁
審校 | 重樓
開發者中普遍存在一種誤解,認為一旦刪除了提交(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

























