Go 部落格
防遍歷檔案API
當攻擊者能夠誘騙程式開啟並非其預期檔案時,就會出現路徑遍歷漏洞。本文解釋了這類漏洞、一些現有防禦措施,並描述了Go 1.24中新增的os.Root
API如何提供簡單而強大的防禦,以防止意外的路徑遍歷。
路徑遍歷攻擊
“路徑遍歷”涵蓋了遵循共同模式的一系列相關攻擊:程式嘗試在某個已知位置開啟檔案,但攻擊者導致它在不同位置開啟檔案。
如果攻擊者控制檔名的一部分,他們可能會使用相對目錄元件(“..”)來逃逸預期位置。
f, err := os.Open(filepath.Join(trustedLocation, "../../../../etc/passwd"))
在Windows系統上,某些名稱具有特殊含義。
// f will print to the console.
f, err := os.Create(filepath.Join(trustedLocation, "CONOUT$"))
如果攻擊者控制本地檔案系統的一部分,他們可能會使用符號連結導致程式訪問錯誤的檔案。
// Attacker links /home/user/.config to /home/otheruser/.config:
err := os.WriteFile("/home/user/.config/foo", config, 0o666)
如果程式透過首先驗證預期檔案不包含任何符號連結來防禦符號連結遍歷,它仍然可能容易受到檢查時與使用時(TOCTOU)競爭條件的影響,即攻擊者在程式檢查後建立符號連結。
// Validate the path before use.
cleaned, err := filepath.EvalSymlinks(unsafePath)
if err != nil {
return err
}
if !filepath.IsLocal(cleaned) {
return errors.New("unsafe path")
}
// Attacker replaces part of the path with a symlink.
// The Open call follows the symlink:
f, err := os.Open(cleaned)
另一種TOCTOU競爭涉及在遍歷過程中移動構成路徑一部分的目錄。例如,攻擊者提供了一個路徑,如“a/b/c/../../etc/passwd”,並在開啟操作進行中時將“a/b/c”重新命名為“a/b”。
路徑清理
在我們普遍解決路徑遍歷攻擊之前,讓我們從路徑清理開始。當程式的威脅模型不包括擁有本地檔案系統訪問許可權的攻擊者時,在使用不受信任的輸入路徑之前對其進行驗證就足夠了。
不幸的是,清理路徑可能出奇地棘手,特別是對於必須同時處理Unix和Windows路徑的可移植程式。例如,在Windows上,filepath.IsAbs(`\foo`)
報告false
,因為路徑“\foo”是相對於當前驅動器的。
在Go 1.20中,我們添加了path/filepath.IsLocal
函式,它報告路徑是否為“本地”路徑。“本地”路徑是指
- 不逃逸其被評估的目錄(不允許“../etc/passwd”);
- 不是絕對路徑(不允許“/etc/passwd”);
- 不為空(不允許“”);
- 在Windows上,不是保留名稱(不允許“COM1”)。
在Go 1.23中,我們添加了path/filepath.Localize
函式,它將以/分隔的路徑轉換為本地作業系統路徑。
接受並操作可能由攻擊者控制的路徑的程式,幾乎總是應該使用filepath.IsLocal
或filepath.Localize
來驗證或清理這些路徑。
超越清理
當攻擊者可能訪問本地檔案系統的一部分時,路徑清理是不夠的。
多使用者系統如今並不常見,但攻擊者對檔案系統的訪問仍可透過多種方式發生。一個解壓tar或zip檔案的解壓工具可能會被誘導解壓一個符號連結,然後解壓一個遍歷該連結的檔名。容器執行時可能會授予不受信任的程式碼對本地檔案系統一部分的訪問許可權。
程式可以透過使用path/filepath.EvalSymlinks
函式在驗證之前解析不受信任名稱中的連結來防禦意外的符號連結遍歷,但如上所述,這種兩步過程容易受到TOCTOU競爭條件的影響。
在Go 1.24之前,更安全的選項是使用github.com/google/safeopen等包,它提供了防路徑遍歷函式,用於在特定目錄中開啟可能不受信任的檔名。
引入os.Root
在Go 1.24中,我們將在os
包中引入新的API,以安全地、以防遍歷的方式在某個位置開啟檔案。
新的os.Root
型別表示本地檔案系統中的某個目錄。使用os.OpenRoot
函式開啟一個根:
root, err := os.OpenRoot("/some/root/directory")
if err != nil {
return err
}
defer root.Close()
Root
提供了在根目錄內操作檔案的方法。這些方法都接受相對於根目錄的檔名,並禁止任何使用相對路徑元件(“..”)或符號連結逃逸根目錄的操作。
f, err := root.Open("path/to/file")
Root
允許不逃逸根目錄的相對路徑元件和符號連結。例如,root.Open("a/../b")
是允許的。檔名使用本地平臺的語義解析:在Unix系統上,這將遵循“a”中的任何符號連結(只要該連結不逃逸根目錄);而在Windows系統上,這將開啟“b”(即使“a”不存在)。
Root
目前提供以下操作集:
func (*Root) Create(string) (*File, error)
func (*Root) Lstat(string) (fs.FileInfo, error)
func (*Root) Mkdir(string, fs.FileMode) error
func (*Root) Open(string) (*File, error)
func (*Root) OpenFile(string, int, fs.FileMode) (*File, error)
func (*Root) OpenRoot(string) (*Root, error)
func (*Root) Remove(string) error
func (*Root) Stat(string) (fs.FileInfo, error)
除了Root
型別,新的os.OpenInRoot
函式提供了一種簡單的方法,可以在特定目錄中開啟可能不受信任的檔名。
f, err := os.OpenInRoot("/some/root/directory", untrustedFilename)
Root
型別為處理不受信任的檔名提供了一個簡單、安全、可移植的API。
注意事項和考量
Unix
在Unix系統上,Root
使用openat
系統呼叫族實現。一個Root
包含一個檔案描述符,引用其根目錄,並將在重新命名或刪除後跟蹤該目錄。
Root
防禦符號連結遍歷,但不限制掛載點的遍歷。例如,Root
不阻止Linux繫結掛載的遍歷。我們的威脅模型是Root
防禦普通使用者可能建立的檔案系統構造(如符號連結),但不處理需要root許可權才能建立的構造(如繫結掛載)。
Windows
在Windows上,Root
開啟一個引用其根目錄的控制代碼。在Root
關閉之前,該開啟的控制代碼會阻止該目錄被重新命名或刪除。
Root
阻止訪問保留的Windows裝置名稱,例如NUL
和COM1
。
WASI
在WASI上,os
包使用WASI預覽版1檔案系統API,這些API旨在提供防遍歷的檔案系統訪問。然而,並非所有WASI實現都完全支援檔案系統沙盒,並且Root
對遍歷的防禦僅限於WASI實現提供的防禦。
GOOS=js
當GOOS=js時,os
包使用Node.js檔案系統API。此API不包括openat系列函式,因此os.Root
在該平臺上容易受到符號連結驗證中的TOCTOU(檢查時與使用時)競爭條件的影響。
當GOOS=js時,Root
引用目錄名而不是檔案描述符,並且在重新命名後不跟蹤目錄。
Plan 9
Plan 9沒有符號連結。在Plan 9上,Root
引用目錄名並對檔名執行詞法清理。
效能
包含許多目錄元件的檔名上的Root
操作可能比等效的非Root
操作昂貴得多。解析“..”元件也可能很昂貴。希望限制檔案系統操作成本的程式可以使用filepath.Clean
從輸入檔名中刪除“..”元件,並且可能希望限制目錄元件的數量。
誰應該使用os.Root?
如果您符合以下條件,則應使用os.Root
或os.OpenInRoot
:
- 您正在目錄中開啟檔案;並且
- 該操作不應訪問該目錄之外的檔案。
例如,將檔案寫入輸出目錄的存檔提取器應使用os.Root
,因為檔名可能不受信任,並且將檔案寫入輸出目錄之外是不正確的。
然而,將輸出寫入使用者指定位置的命令列程式不應使用os.Root
,因為檔名不是不受信任的,並且可能指代檔案系統上的任何位置。
一個好的經驗法則是,呼叫filepath.Join
將固定目錄和外部提供的檔名組合起來的程式碼,很可能應該改用os.Root
。
// This might open a file not located in baseDirectory.
f, err := os.Open(filepath.Join(baseDirectory, filename))
// This will only open files under baseDirectory.
f, err := os.OpenInRoot(baseDirectory, filename)
未來工作
os.Root
API是Go 1.24中新增的。我們預計在未來的版本中對其進行新增和改進。
當前的實現優先考慮正確性和安全性而非效能。未來的版本將利用平臺特定的API(如Linux的openat2
)來儘可能提高效能。
Root
目前尚不支援許多檔案系統操作,例如建立符號連結和重新命名檔案。在可能的情況下,我們將新增對這些操作的支援。正在進行的額外函式列表請參見go.dev/issue/67002。