Go 如何減少供應鏈攻擊?
無論采用何種過程或技術手段,每個依賴性都必然存在著相互信任的關系。但是,Go 的工具和設計幫助降低了所有階段的風險。
所有構建都已“鎖定”
外部世界的變化,例如發布依賴性的新版本,并不會影響 Go 的構建。
與大多數軟件包管理器文件不同,Go 模塊沒有單獨的約束列表和鎖文件,但是它鎖定了某個特定的版本。任何 Go 構建的每個依賴性的版本完全取決于主模塊的 go.mod 文件。
從 Go 1.16 開始,這種決定論就會強制執行,并且在 go.mod 不完整的情況下,構建命令(gobuild、gotest、goinstall、gorun……)將會失敗。唯一會改變 go.mod(因此也會改變構建)的命令是 goget 和 gomodtidy。這些命令不會被自動或在 CI 中運行,所以對依賴樹的改變必須是故意的,并且有機會通過代碼審查。
這對安全非常重要,因為當 CI 系統或新機器運行時,簽入 (checked-in) 的源碼是最終的和完整的,代碼將說明什么會被構建,第三方沒有辦法影響它。
此外,當用 goget 添加依賴性時,由于最小版本的選擇,它的交叉依賴會按照依賴的 go.mod 文件中指定的版本添加,而不是按照最新版本。同樣的情況也發生在調用 goinstallexample.com/cmd/devtoolx@latest 的情況下,在某些生態系統中,它的等價物會繞過 pinning。在 Go 中,example.com/cmd/devtoolx 的最新版本將被獲取,但所有的依賴性將由其 go.mod 文件設置。
如果一個模塊被破壞,新的惡意版本被發布,在它們明確更新該依賴性之前,不會受到任何影響,這就提供了審查更改的機會,并讓生態系統有了足夠的時間來檢測事件。
版本內容永遠不會改變
確保第三方不能影響構建的另一個關鍵屬性是,模塊版本的內容是不可改變的。如果攻擊者破壞了依賴性,可以重新上傳現有的版本,他們就可以自動破壞所有依賴它的項目。
這就是 go.sum 文件的作用。它包含構建所需的每個依賴項的加密哈希列表。同樣,一個不完整的 go.sum 會導致錯誤,而且只有 goget 和 gomod tidy 才會修改它,所以任何對它的修改都會伴隨著故意的依賴性改變。其他的構建被保證有一套完整的校驗和。
這是大多數鎖文件的一個共同特征。Go 通過 Checksum Database(簡稱 sumdb)超越了它,它是一個全局性的、僅可附加的加密驗證的 go.sum 條目列表。當 goget 需要在 go.sum 文件中添加一個條目時,它從 sumdb 中獲取該條目,并對 sumdb 的完整性進行加密證明。這不僅確保了某一模塊的每一次構建都使用相同的依賴內容,而且確保了每一個模塊都使用相同的依賴內容。
sumdb 使得被破壞的依賴內容,甚至谷歌運營的 Go 基礎設施不可能用修改過的(例如 backdoored)源代碼來針對特定的依賴內容。保證你使用的代碼與其他使用例如 example.com/modulex 的 v1.9.2 的人所使用的代碼完全一樣,并且已通過審查。
最后,我最喜歡 sumdb 的特性是:它不需要模塊作者的任何密鑰管理,并且可以無縫地與 Go 模塊的去中心化特性配合使用。
VCS 是真理的源泉
大多數項目是通過某種版本控制系統(VCS)開發的,然后在其他生態系統中,上傳到包存儲庫。這意味著有兩個賬戶可能被入侵,即 VCS 主機和包存儲庫,后者使用得更少,更容易被忽視。這也意味著在上傳到存儲庫的版本中更容易隱藏惡意代碼,尤其是在上傳過程中經常修改源代碼的情況下,比如說將其最小化。
在 Go 中,不存在包存儲庫賬戶這樣的東西。包的導入路徑嵌入了 gomoddownload 所需要的信息,以便直接從 VCS 中獲取其模塊,其中標簽定義了版本。
我們確實有 Go Module Mirror,但那只是一個代理。模塊作者不需要注冊賬戶,也不需要向代理上傳版本。代理使用與 go 工具相同的邏輯(事實上,代理運行 gomoddownload)來獲取和緩存版本。由于校驗數據庫保證給定的模塊版本只能有一個源樹,每個使用代理的人都會看到與繞過代理直接從 VCS 獲取的結果相同。(如果該版本在 VCS 中不再可用,或者其內容發生了變化,直接獲取將導致錯誤,而從代理獲取可能仍然有效,提高了可用性并保護生態系統免受 “左鍵”問題的影響)。
在客戶端運行 VCS 工具會暴露出一個相當大的攻擊面。這也是 Go Module Mirror 的另一個作用:代理上的 Go 工具在一個強大的沙盒內運行,并被配置為支持所有的 VCS 工具,而默認的是只支持兩個主要的 VCS 系統(git 和 Mercurial)。任何使用代理的人仍然可以獲取使用非默認的 VCS 系統發布的代碼,但攻擊者在大多數安裝中無法接觸到這些代碼。
僅構建代碼,但不會執行它
Go 工具鏈的一個清晰的安全設計目標是,即使代碼是不可信和惡意的,也不能獲取或構建代碼來執行該代碼。這與大多數生態系統不同,其中許多生態系統對在獲取包時運行代碼提供了一流的支持。這些“安裝后”的鉤子在在過去被用作一種最方便的攻擊方式:通過受到攻擊的依賴攻擊開發者的機器,并通過 module 作者進行蠕蟲攻擊。
公平地說,如果你要獲取一些代碼,往往會在不久之后執行,要么作為開發者機器上測試的一部分,要么作為生產中二進制文件的一部分,所以缺乏安裝后鉤子只會減緩攻擊者。(在構建過程中沒有安全邊界:任何有助于構建的軟件包都可以定義一個初始函數)。然而,這也是一種有意義的風險緩解,因為你可能在執行一個二進制文件或測試一個包時,只使用了模塊依賴的一個子集。例如,如果你在 macOS 上構建并執行 example.com/cmd/devtoolx,那么只有 Windows 的依賴或 example.com/cmd/othertool 的依賴就不可能危害到你的機器。
在 Go 中,不為特定構建提供代碼的模塊對其沒有安全影響。
“一點復制比一點依賴要好”
在 Go 生態系統中,最后一個也許也是最重要的軟件供應鏈風險緩解措施是最沒有技術含量的一個:Go 有一種拒絕大型依賴樹的文化,寧愿復制一下也不愿意添加新的依賴。這可以追溯到 Go 的一個諺語:“一點復制比一點依賴要好”。高質量的可重用 Go 模塊自豪地戴上了 “零依賴” 的標簽。如果你發現自己需要一個庫,你很可能會發現它不會導致你依賴其他作者和所有者的幾十個模塊。
豐富的標準庫和其他模塊(golang.org/x/……的模塊)也支持這一點,這些模塊提供了常用的高級構建模塊,如 HTTP 棧、TLS 庫、JSON 編碼等。
所有這些意味著只需少量的依賴性就可以建立豐富、復雜的應用程序。無論工具有多好,它都不能消除重復使用代碼的風險,所以最有力的緩解措施永遠是一個小的依賴樹。