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 基礎架構都無法針對特定依賴項使用修改後的(例如,帶有後門的)原始碼。您可以確定自己使用的是與所有使用例如 v1.9.2 版本的 example.com/modulex
的人使用並審查過的程式碼完全相同。
最後,sumdb 的我最喜歡的功能是:它不需要模組作者進行任何金鑰管理,並且它與 Go 模組的去中心化特性無縫協作。
VCS 是真相來源
大多數專案都透過某種版本控制系統(VCS)進行開發,然後在其他生態系統中上傳到包儲存庫。這意味著有兩個帳戶可能被破壞,VCS 主機和包儲存庫,後者使用頻率較低,並且更有可能被忽略。這也意味著更容易在上傳到儲存庫的版本中隱藏惡意程式碼,特別是如果原始碼在上傳過程中被常規修改,例如為了最小化它。
在 Go 中,不存在包儲存庫帳戶之類的東西。包的匯入路徑嵌入了 go mod download
需要直接從 VCS 獲取其模組 的資訊,其中標籤定義了版本。
我們確實有 Go Module Mirror,但它只是一個代理。模組作者不註冊帳戶,也不向代理上傳版本。代理使用與 go
工具相同的邏輯(實際上,代理執行 go mod download
)來獲取和快取版本。由於校驗和資料庫保證了給定模組版本只能有一個源樹,因此使用代理的每個人都將看到與繞過代理直接從 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 模組自豪地佩戴。如果你發現自己需要一個庫,你很可能會發現它不會讓你承擔數十個其他作者和所有者的模組的依賴。
這得益於豐富的標準庫和附加模組(golang.org/x/...
),它們提供了常用的高階構建塊,例如 HTTP 堆疊、TLS 庫、JSON 編碼等。
總而言之,這意味著可以僅使用少數幾個依賴項來構建豐富、複雜的應用程式。無論工具多麼好,它都無法消除重用程式碼所涉及的風險,因此最強大的緩解措施將始終是小型依賴項樹。