Go 部落格
Go 如何減輕供應鏈攻擊
現代軟體工程是協作式的,並且基於重複使用開放原始碼軟體。這使得軟體專案容易受到供應鏈攻擊的目標,攻擊者會透過入侵其依賴項來攻擊軟體專案。
儘管採取任何流程或技術措施,每個依賴項都不可避免地是一種信任關係。然而,Go 的工具和設計有助於在各個階段降低風險。
所有建置都被「鎖定」
外部世界的變化(例如發佈依賴項的新版本)無法自動影響 Go 建置。
與大多數其他套件管理器檔案不同,Go 模組沒有單獨的約束清單和鎖定檔案來固定特定版本。任何 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
的呼叫也是如此,在某些生態系統中,等效的呼叫會繞過固定。 在 Go 中,將會提取 example.com/cmd/devtoolx
的最新版本,但隨後所有依賴項都將由其 go.mod
檔案設定。
如果模組遭到入侵並發佈了新的惡意版本,則在明確更新該依賴項之前,沒有人會受到影響,這提供了審查更改的機會,並讓生態系統有時間檢測事件。
版本內容永不變更
確保第三方無法影響建置的另一個關鍵屬性是模組版本的內容是不可變的。如果入侵依賴項的攻擊者可以重新上傳現有版本,則他們可以自動入侵所有依賴於它的專案。
這就是 go.sum
檔案 的用途。它包含建置的所有依賴項的加密雜湊清單。同樣,不完整的 go.sum
會導致錯誤,並且只有 go get
和 go mod tidy
會修改它,因此對它的任何更改都將伴隨著刻意的依賴項更改。其他建置保證具有完整的校驗和集。
這是大多數鎖定檔案的常見功能。Go 更進一步,使用 校驗和資料庫(簡稱 sumdb),這是一個全域的僅附加加密可驗證的 go.sum 項目清單。當 go get
需要向 go.sum
檔案新增項目時,它會從 sumdb 中提取它以及 sumdb 完整性的加密證明。這確保了不僅特定模組的每個建置都使用相同的依賴項內容,而且每個模組都使用相同的依賴項內容!
sumdb 使得受感染的依賴項甚至 Google 運營的 Go 基礎架構無法使用修改過的(例如後門)原始碼來鎖定特定依賴項。您保證使用與其他使用例如 example.com/modulex
的 v1.9.2 的人相同的程式碼,並且已經過審查。
最後,我最喜歡的 sumdb 功能:它不需要模組作者進行任何金鑰管理,並且可以與 Go 模組的分散式特性無縫地協作。
版本控制系統是事實的來源
大多數專案都是透過某種版本控制系統 (VCS) 開發的,然後在其他生態系統中上傳到套件儲存庫。這意味著可能有兩個帳戶遭到入侵,VCS 主機和套件儲存庫,後者使用頻率較低,更容易被忽視。這也意味著更容易在上傳到儲存庫的版本中隱藏惡意程式碼,尤其是在上傳過程中經常修改原始碼的情況下,例如為了最小化它。
在 Go 中,沒有套件儲存庫帳戶之類的東西。套件的匯入路徑嵌入了 go mod download
從 VCS 直接提取其模組所需的資訊,其中標籤定義版本。
我們確實有 Go 模組鏡像,但那只是一個代理。模組作者不會註冊帳戶,也不會將版本上傳到代理。代理使用與 go
工具相同的邏輯(事實上,代理執行 go mod download
)來提取和快取版本。由於校驗和資料庫保證對於給定的模組版本只能有一個原始碼樹,因此使用代理的每個人都會看到與繞過它並直接從 VCS 提取的人相同的結果。(如果版本在 VCS 中不再可用或其內容已更改,則直接提取將導致錯誤,而從代理提取可能仍然有效,從而提高可用性並保護生態系統免受 “left-pad” 問題 的影響。)
在客戶端上運行 VCS 工具會暴露相當大的攻擊面。這是 Go 模組鏡像的另一個幫助:代理上的 go
工具在強大的沙盒中運行,並且配置為支援每種 VCS 工具,而 預設情況下僅支援兩個主要的 VCS 系統(git 和 Mercurial)。任何使用代理的人仍然可以提取使用預設情況下關閉的 VCS 系統發佈的程式碼,但攻擊者在大多数安裝中無法訪問該程式碼。
建置程式碼不會執行它
Go 工具鏈的一個明確的安全設計目標是,即使程式碼不受信任且是惡意的,提取或建置程式碼也不會讓該程式碼執行。這與大多數其他生態系統不同,許多生態系統都原生支援在套件提取時運行程式碼。這些“安裝後”掛鉤過去曾被用作將受感染的依賴項變成受感染的開發者機器的最便捷方式,以及 蠕蟲 透過模組作者的方式。
公平地說,如果您要提取一些程式碼,通常會在不久之後執行它,無論是在開發者機器上進行測試還是在生產環境中作為二進制檔案的一部分,因此缺少安裝後掛鉤只會減慢攻擊者的速度。(建置中沒有安全邊界:任何有助於建置的套件都可以定義 init
函數。)但是,它可以是一種有意義的風險降低措施,因為您可能正在執行二進制檔案或測試僅使用模組依賴項子集的套件。例如,如果您在 macOS 上建置並執行 example.com/cmd/devtoolx
,則 Windows 專用依賴項或 example.com/cmd/othertool
的依賴項無法入侵您的機器。
在 Go 中,未對特定建置提供程式碼的模組對其沒有安全影響。
「少量複製勝過少量依賴」
Go 生態系統中最後也是最重要的軟體供應鏈風險降低措施是最不技術性的:Go 有一種拒絕大型依賴項樹的文化,並且更喜歡複製一點而不是新增新的依賴項。它可以追溯到 Go 的一句諺語:“少量複製勝過少量依賴”。標籤“零依賴項”自豪地被高品質的可重複使用的 Go 模組所佩戴。如果您發現自己需要一個函式庫,您可能會發現它不會導致您依賴於其他作者和所有者的數十個其他模組。
這也得益於豐富的標準函式庫和其他模組(golang.org/x/...
模組),它們提供了常用的高階建置模組,例如 HTTP 堆疊、TLS 函式庫、JSON 編碼等。
總而言之,這意味著可以只使用少數幾個依賴項來建置豐富、複雜的應用程式。無論工具有多好,它都無法消除重複使用程式碼所涉及的風險,因此最強大的緩解措施始終是小型依賴項樹。