Go 部落格
Go 如何緩解供應鏈攻擊
現代軟體工程是協作式的,並基於對開源軟體的重用。這使得目標容易受到供應鏈攻擊,即透過損害軟體專案的依賴來攻擊專案本身。
儘管採取了任何流程或技術措施,每個依賴項都不可避免地是一種信任關係。然而,Go 的工具和設計有助於在各個階段緩解風險。
所有構建都是“鎖定”的
外部世界(例如依賴項釋出新版本)的更改無法自動影響 Go 構建。
與大多數其他包管理器的檔案不同,Go modules 沒有獨立的約束列表和鎖定特定版本的 lock 檔案。任何 Go 構建所依賴的每個依賴項的版本完全由主模組的 go.mod
檔案決定。
從 Go 1.16 開始,預設強制執行這種確定性,並且構建命令(go build
、go test
、go install
、go run
等)如果 go.mod 不完整將會失敗。唯一會更改 go.mod
(以及隨之更改構建)的命令是 go get
和 go mod tidy
。這些命令預計不會自動或在 CI 中執行,因此對依賴樹的更改必須是故意的,並且有機會透過程式碼審查。
這對安全性非常重要,因為當 CI 系統或新機器執行 go build
時,已簽入的原始碼是構建內容的最終且完整的真相來源。第三方無法影響它。
此外,使用 go get
新增依賴項時,其傳遞依賴項會按照依賴項的 go.mod
檔案中指定的版本新增,而不是最新版本,這得益於最小版本選擇。對於呼叫 go install example.com/cmd/devtoolx@latest
也是如此,這在某些生態系統中相當於繞過了 pinning。在 Go 中,會獲取 example.com/cmd/devtoolx
的最新版本,但其所有依賴項都會由其 go.mod
檔案設定。
如果一個模組被攻陷併發布了新的惡意版本,在使用者明確更新該依賴項之前,沒有人會受到影響,這提供了審查更改的機會以及生態系統檢測到該事件的時間。
版本內容永不更改
確保第三方無法影響構建的另一個關鍵屬性是模組版本的內容不可變。如果攻擊者攻陷一個依賴項後可以重新上傳現有版本,他們就可以自動攻陷所有依賴於該版本的專案。
這就是 go.sum
檔案的作用。它包含構成構建的每個依賴項的加密雜湊列表。同樣,不完整的 go.sum
會導致錯誤,並且只有 go get
和 go mod tidy
會修改它,因此對其的任何更改都會伴隨故意的依賴項更改。其他構建則保證擁有一整套校驗和。
這是大多數 lock 檔案的常見功能。Go 透過 Checksum Database(簡稱 sumdb)超越了這一點,sumdb 是一個全域性的、僅追加的、可加密驗證的 go.sum 條目列表。當 go get
需要向 go.sum
檔案新增條目時,它會從 sumdb 獲取該條目,並附帶 sumdb 完整性的加密證明。這確保了不僅特定模組的每個構建都使用相同的依賴項內容,而且所有模組都使用相同的依賴項內容!
sumdb 使得被攻陷的依賴項甚至 Google 運營的 Go 基礎設施無法用修改後的(例如後門)原始碼針對特定依賴者。您被保證使用的是與所有其他使用例如 example.com/modulex
v1.9.2 版本的人完全相同的程式碼,並且該程式碼已經過審查。
最後,我最喜歡的 sumdb 功能是:它不需要模組作者進行任何金鑰管理,並且與 Go modules 的去中心化特性無縫整合。
VCS 是真相來源
大多數專案透過版本控制系統(VCS)開發,然後在其他生態系統中上傳到包倉庫。這意味著存在兩個可能被攻陷的賬戶:VCS 主機和包倉庫,後者使用頻率較低,更容易被忽視。這也意味著更容易在上傳到倉庫的版本中隱藏惡意程式碼,特別是如果原始碼在上傳過程中被常規修改,例如進行壓縮。
在 Go 中,不存在包倉庫賬戶。包的匯入路徑嵌入了 go mod download
直接從 VCS 獲取其模組所需的資訊,VCS 中的標籤定義了版本。
我們確實有 Go Module Mirror,但這只是一個代理。模組作者無需註冊賬戶,也無需將版本上傳到代理。代理使用與 go
工具相同的邏輯(實際上,代理執行 go mod download
)來獲取和快取版本。由於 Checksum Database 保證給定模組版本只能有一個原始碼樹,因此使用代理的每個人都會看到與繞過代理直接從 VCS 獲取相同的結果。(如果 VCS 中不再有該版本或其內容發生變化,直接獲取將導致錯誤,而從代理獲取可能仍然有效,從而提高了可用性並保護生態系統免受 “left-pad”問題的影響。)
在客戶端執行 VCS 工具會暴露相當大的攻擊面。這是 Go Module Mirror 提供幫助的另一個方面:代理上的 go
工具在一個強大的沙箱內執行,並配置為支援所有 VCS 工具,而預設情況下僅支援兩種主要的 VCS 系統(git 和 Mercurial)。使用代理的任何人仍然可以獲取使用預設關閉的 VCS 系統釋出的程式碼,但在大多數安裝中攻擊者無法觸及這些程式碼。
構建程式碼不執行程式碼
Go 工具鏈的一個明確的安全設計目標是,無論獲取還是構建程式碼,即使程式碼不受信任且是惡意的,都不會執行該程式碼。這與大多數其他生態系統不同,許多其他生態系統對包獲取時執行程式碼提供一流支援。這些“安裝後”鉤子過去曾被用作最便捷的方式,將受損的依賴項變成受損的開發人員機器,並透過模組作者傳播蠕蟲。
公平地說,如果你正在獲取某些程式碼,通常很快就會執行它,無論是作為開發人員機器上測試的一部分還是生產環境中二進位制檔案的一部分,因此缺少安裝後鉤子只會減緩攻擊者的速度。(構建內部沒有安全邊界:任何構成構建的包都可以定義 init
函式。)然而,這可以是一個有意義的風險緩解措施,因為你可能正在執行一個二進位制檔案或測試一個僅使用模組部分依賴項的包。例如,如果你在 macOS 上構建並執行 example.com/cmd/devtoolx
,那麼一個僅限 Windows 的依賴項或 example.com/cmd/othertool
的依賴項沒有任何方式來攻擊你的機器。
在 Go 中,不為特定構建貢獻程式碼的模組對該構建沒有安全影響。
“少量複製勝於少量依賴”
Go 生態系統中最後也是也許最重要的軟體供應鏈風險緩解措施是最不技術的:Go 有一種拒絕大型依賴樹的文化,並且傾向於少量複製而不是新增新的依賴項。這可以追溯到 Go 的一個諺語:“少量複製勝於少量依賴”。高質量的可重用 Go 模組 proudly 佩戴著“零依賴”的標籤。如果你需要一個庫,你可能會發現它不會讓你依賴於其他作者和所有者的數十個其他模組。
這也是得益於豐富的標準庫和附加模組(如 golang.org/x/...
系列),它們提供了常用的高階構建塊,例如 HTTP 棧、TLS 庫、JSON 編碼等。
總而言之,這意味著只需少量依賴項即可構建豐富、複雜的應用程式。無論工具多麼優秀,它都無法消除重用程式碼所帶來的風險,因此最強的緩解措施永遠是保持較小的依賴樹。