Go 部落格
Go 命令 PATH 安全性
今天釋出的 Go 安全更新 修復了一個涉及在不可信目錄中進行 PATH 查詢的問題,該問題可能導致在執行 go
get
命令期間發生遠端程式碼執行。我們預計大家會對此具體含義以及自己的程式是否可能存在類似問題感到疑問。本文將詳細介紹該漏洞、我們已應用的修復措施、如何判斷你的程式是否容易受到類似問題的影響,以及如果存在此類問題可以採取的措施。
Go 命令與遠端執行
go
命令的設計目標之一是,大多數命令——包括 go
build
、go
doc
、go
get
、go
install
和 go
list
——不會執行從網際網路下載的任意程式碼。有一些明顯的例外:顯然 go
run
、go
test
和 go
generate
確實會執行任意程式碼——這是它們的作用。但其他命令則不能,原因包括可重現構建和安全性。因此,當 go
get
可以被欺騙執行任意程式碼時,我們認為這是一個安全漏洞。
如果 go
get
不能執行任意程式碼,那麼不幸的是,這意味著它呼叫的所有程式,例如編譯器和版本控制系統,也都位於安全邊界之內。例如,我們過去曾遇到過一些問題,其中巧妙地利用晦澀的編譯器特性或版本控制系統中的遠端執行漏洞,最終變成了 Go 中的遠端執行漏洞。(順便提一下,Go 1.16 旨在透過引入 GOVCS 設定來改善這種情況,該設定允許精確配置允許哪些版本控制系統以及何時允許。)
然而,今天的漏洞完全是我們的過錯,而不是 gcc
或 git
的錯誤或晦澀特性。該漏洞涉及 Go 和其他程式如何查詢其他可執行檔案,因此我們需要花一些時間瞭解這一點,然後才能深入細節。
命令、PATH 與 Go
所有作業系統都有一個可執行路徑的概念(Unix 上是 $PATH
,Windows 上是 %PATH%
;為簡化起見,我們只使用術語 PATH),它是一個目錄列表。當你在 shell 提示符中輸入命令時,shell 會依次在列表中每個目錄中查詢與你輸入的名稱相同的可執行檔案。它會執行找到的第一個檔案,或者打印出類似“命令未找到”的訊息。
在 Unix 上,這個概念首次出現在第七版 Unix 的 Bourne shell (1979) 中。手冊解釋道:
shell 引數
$PATH
定義了包含命令的目錄搜尋路徑。每個備選目錄名都以冒號 (:
) 分隔。預設路徑是:/bin:/usr/bin
。如果命令名包含 /,則不使用搜索路徑。否則,將在路徑中的每個目錄中搜索可執行檔案。
注意預設值:當前目錄(此處用空字串表示,但我們稱之為“點”,即 '.')列在 /bin
和 /usr/bin
之前。MS-DOS 以及後來的 Windows 選擇硬編碼了這種行為:在這些系統上,“點”總是在考慮 %PATH%
中列出的任何目錄之前自動進行搜尋。
正如 Grampp 和 Morris 在他們的經典論文“UNIX 作業系統安全性”(1984 年)中所指出的,將“點”放在系統目錄之前意味著如果你 cd
進入一個目錄並執行 ls
,你可能會在該目錄中找到惡意副本,而不是系統工具。如果你能誘騙系統管理員在以 root
使用者身份登入時在你的主目錄中執行 ls
,那麼你就可以執行任何你想要的程式碼。由於這個問題以及其他類似問題,幾乎所有現代 Unix 發行版都將新使用者的預設 PATH 設定為不包含“點”。但無論 PATH 如何設定,Windows 系統都會繼續優先搜尋“點”。
例如,當你在命令列中輸入命令
go version
在典型的 Unix 配置下,shell 會從 PATH 中的系統目錄執行 go
可執行檔案。但當你在 Windows 上輸入該命令時,cmd.exe
會先檢查“點”目錄。如果 .\go.exe
(或 .\go.bat
或許多其他選項)存在,cmd.exe
會執行該可執行檔案,而不是 PATH 中的那個。
對於 Go,PATH 搜尋由 exec.LookPath
處理,該函式由 exec.Command
自動呼叫。為了更好地適應宿主系統,Go 的 exec.LookPath
在 Unix 上實現了 Unix 規則,在 Windows 上實現了 Windows 規則。例如,此命令
out, err := exec.Command("go", "version").CombinedOutput()
其行為與在作業系統 shell 中輸入 go
version
相同。在 Windows 上,如果 .\go.exe
存在,它就會執行它。
(值得注意的是,Windows PowerShell 改變了這種行為,取消了對“點”的隱式搜尋,但 cmd.exe
和 Windows C 庫的 SearchPath 函式
繼續保持原有的行為。Go 繼續與 cmd.exe
的行為一致。)
漏洞
當 go
get
下載並構建包含 import
"C"
的包時,它會執行一個名為 cgo
的程式,以準備相應的 C 程式碼的 Go 等價物。go
命令在包含包原始檔的目錄中執行 cgo
。一旦 cgo
生成了 Go 輸出檔案,go
命令本身就會對生成的 Go 檔案呼叫 Go 編譯器,並對包中包含的任何 C 原始碼呼叫宿主 C 編譯器(gcc
或 clang
)進行構建。所有這些都執行良好。但是 go
命令在哪裡找到宿主 C 編譯器呢?當然是在 PATH 中查詢。幸運的是,儘管它在包源目錄中執行 C 編譯器,但它是在呼叫 go
命令的原始目錄中進行 PATH 查詢的
cmd := exec.Command("gcc", "file.c")
cmd.Dir = "badpkg"
cmd.Run()
因此,即使 Windows 系統上存在 badpkg\gcc.exe
,這段程式碼片段也不會找到它。exec.Command
中進行的查詢不知道 badpkg
目錄。
go
命令使用類似的程式碼來呼叫 cgo
,在這種情況下甚至沒有路徑查詢,因為 cgo
總是來自 GOROOT
cmd := exec.Command(GOROOT+"/pkg/tool/"+GOOS_GOARCH+"/cgo", "file.go")
cmd.Dir = "badpkg"
cmd.Run()
這比前面的程式碼片段更安全:不存在執行任何可能存在的惡意 cgo.exe
的風險。
但事實證明,cgo 本身也會在其建立的一些臨時檔案上呼叫宿主 C 編譯器,這意味著它會執行這段程式碼
// running in cgo in badpkg dir
cmd := exec.Command("gcc", "tmpfile.c")
cmd.Run()
現在,由於 cgo 本身正在 badpkg
中執行,而不是在執行 go
命令的目錄中執行,如果該檔案存在,它將執行 badpkg\gcc.exe
,而不是查詢系統中的 gcc
。
因此,攻擊者可以建立一個使用 cgo 幷包含 gcc.exe
的惡意包,然後任何在 Windows 上執行 go
get
下載並構建攻擊者包的使用者都將優先執行攻擊者提供的 gcc.exe
,而不是系統路徑中的任何 gcc
。
Unix 系統避免這個問題,首先是因為“點”通常不在 PATH 中,其次是因為模組解壓不會對寫入的檔案設定執行位。但是,在 PATH 中將“點”放在系統目錄之前並使用 GOPATH 模式的 Unix 使用者,其易受攻擊性將與 Windows 使用者一樣。(如果這是你的情況,今天是個好日子,你應該從你的 PATH 中刪除“點”並開始使用 Go modules。)
修復措施
go
get
命令下載並執行惡意的 gcc.exe
顯然是不可接受的。但允許這種情況發生的實際錯誤是什麼?以及修復措施是什麼?
一種可能的答案是,錯誤在於 cgo
在不可信的源目錄中搜索宿主 C 編譯器,而不是在呼叫 go
命令的目錄中搜索。如果這是錯誤,那麼修復措施就是修改 go
命令,將宿主 C 編譯器的完整路徑傳遞給 cgo
,這樣 cgo
就不需要在不可信目錄中進行 PATH 查找了。
另一種可能的答案是,錯誤在於在 PATH 查詢期間在“點”目錄中進行搜尋,無論這在 Windows 上是自動發生,還是因為 Unix 系統上的明確 PATH 條目。使用者可能希望在“點”目錄中查詢他們在控制檯或 shell 視窗中輸入的命令,但不太可能希望在那裡查詢一個已輸入命令的子程序的子程序。如果這是錯誤,那麼修復措施就是修改 cgo
命令,使其在 PATH 查詢期間不在“點”目錄中進行搜尋。
我們認為兩者都是錯誤,因此應用了雙重修復。go
命令現在將宿主 C 編譯器的完整路徑傳遞給 cgo
。除此之外,cgo
、go
以及 Go 發行版中的所有其他命令現在都使用 os/exec
包的一個變體,如果它之前會使用來自“點”目錄的可執行檔案,則會報告錯誤。包 go/build
和 go/import
在呼叫 go
命令和其他工具時也使用相同的策略。這應該能杜絕任何可能潛藏的類似安全問題。
出於謹慎考慮,我們還在 goimports
和 gopls
等命令以及 golang.org/x/tools/go/analysis
和 golang.org/x/tools/go/packages
庫中進行了類似修復,這些庫會作為子程序呼叫 go
命令。如果你在不可信目錄中執行這些程式——例如,如果你 git
checkout
不可信的倉庫並 cd
進入這些目錄,然後執行這些程式,並且你使用 Windows 或使用 PATH 中包含“點”的 Unix 系統——那麼你也應該更新這些命令的副本。如果你計算機上唯一的不可信目錄是 go
get
管理的模組快取中的目錄,那麼你只需要新的 Go 版本即可。
更新到新的 Go 版本後,你可以使用以下命令更新到最新的 gopls
GO111MODULE=on \
go get golang.org/x/tools/gopls@v0.6.4
你可以使用以下命令更新到最新的 goimports
或其他工具
GO111MODULE=on \
go get golang.org/x/tools/cmd/goimports@v0.1.0
即使在其作者尚未更新之前,你也可以透過在 go
get
期間新增依賴項的明確升級來更新依賴於 golang.org/x/tools/go/packages
的程式
GO111MODULE=on \
go get example.com/cmd/thecmd golang.org/x/tools@v0.1.0
對於使用 go/build
的程式,你只需使用更新後的 Go 版本重新編譯它們即可。
再次強調,只有當你是 Windows 使用者或 PATH 中包含“點”的 Unix 使用者,並且你在不信任的、可能包含惡意程式的源目錄中執行這些程式時,才需要更新這些其他程式。
你的程式是否受影響?
如果你在自己的程式中使用了 exec.LookPath
或 exec.Command
,你只需要擔心在你(或你的使用者)在包含不可信內容的目錄中執行你的程式的情況。如果是這樣,那麼子程序可能會使用來自“點”目錄的可執行檔案啟動,而不是來自系統目錄。(再次強調,在 Windows 上總是會使用來自“點”目錄的可執行檔案,而在 Unix 上只有在不常見的 PATH 設定下才會發生。)
如果你擔心,我們已經將 os/exec
的更嚴格變體釋出為 golang.org/x/sys/execabs
。你只需將
import "os/exec"
替換為
import exec "golang.org/x/sys/execabs"
並重新編譯即可。
預設情況下保護 os/exec 的安全
我們一直在 golang.org/issue/38736 上討論是否應該改變 Windows 在 PATH 查詢期間(使用 exec.Command
和 exec.LookPath
時)總是優先選擇當前目錄的行為。支援改變的論點是它能解決本文討論的這類安全問題。另一個支援的論點是,儘管 Windows 的 SearchPath
API 和 cmd.exe
仍然總是搜尋當前目錄,但 cmd.exe
的後繼者 PowerShell 卻不這樣做,這顯然認識到原始行為是一個錯誤。反對改變的論點是,這可能會破壞現有依賴於在當前目錄中查詢程式的 Windows 程式。我們不知道有多少這樣的程式存在,但如果 PATH 查詢開始完全跳過當前目錄,它們就會出現無法解釋的故障。
我們在 golang.org/x/sys/execabs
中採取的方法可能是一個合理的中間方案。它會找到舊的 PATH 查詢結果,然後返回一個清晰的錯誤,而不是使用來自當前目錄的結果。當 prog.exe
存在時,exec.Command("prog")
返回的錯誤類似於
prog resolves to executable in current directory (.\prog.exe)
對於行為確實發生改變的程式,這個錯誤應該能清楚地說明發生了什麼。打算從當前目錄執行程式的程式可以使用 exec.Command("./prog")
代替(這種語法在所有系統上都有效,包括 Windows)。
我們已將此想法作為一個新提案提出,見 golang.org/issue/43724。
下一篇文章:VS Code Go 擴充套件預設啟用 Gopls
上一篇文章:向 Go 新增泛型的提案
部落格索引