作者 | Filippo Valsorda
譯者 | Sambodhi
策劃 | 钰瑩
現代軟體工程是協作性的,并且基于對開源軟體的重用。這就使目标暴露在供應鍊攻擊之下,而軟體項目則會因為其依賴性被破壞而遭到攻擊。
無論采用何種過程或技術手段,每個依賴性都必然存在着互相信任的關系。但是,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 編碼等。
所有這些意味着隻需少量的依賴性就可以建立豐富、複雜的應用程式。無論工具有多好,它都不能消除重複使用代碼的風險,是以最有力的緩解措施永遠是一個小的依賴樹。
https://go.dev/blog/supply-chain
群内定期釋出網際網路簡報、優質文章等内容。除此之外,我們還會不定期準備 InfoQ 周邊等各種福利送給大家!快來掃碼加入吧~