Avoid path traversal by utlizing Go os.Root

Go os.Root 預防路徑遍歷最佳實踐

前言

最近在撰寫的 Go 後端使用到大量的本地檔案存取,其中最需要被防範的問題之一就是「路徑遍歷」。而在 Go 1.24 新增了 os.Root API 簡潔優雅的避免安全問題。

現有問題

假設要搜尋一個資料夾內的檔案時,最簡單攻擊者可能透過 ../ 跳脫原本預期的資料夾,進而讀取伺服器上的任意檔案。

baseDirectory := "/app/uploads"
filename := "../../../etc/passwd" // BAD PATH
f, err := os.Open(filepath.Join(baseDirectory, filename))

傳統解方

傳統防禦方式通常是驗證並消毒,以避免惡意的路徑:

  • 檢查是否包含 ..
  • 使用 filepath.Clean
  • 驗證前綴
  • 比對絕對路徑
cleaned := filepath.Clean("/app/uploads/" + filename)
if !strings.HasPrefix(cleaned, "/app/uploads/") {
return errors.New("invalid path")
}

但錯誤的操作仍然可能會有安全問題,像是以下的檢查光是少了一個 / 結尾檢查(應該要是 /app/uploads/)就會出現漏洞,因為 /app/uploads 變成 /app/uploads_secret 的字串前綴,但不是其路徑前綴:

filename := "../uploads_secret/flag.txt"
cleaned := filepath.Clean("/app/uploads/" + filename)
if !strings.HasPrefix(cleaned, "/app/uploads") {
return errors.New("invalid path")
}
// filename = /app/uploads_secret/flag.txt

除了繁雜容易出錯的驗證外,還有奇怪的 Symbolic Link、TOCTOU(time-of-check to time-of-use)Race Condition、平台差異⋯⋯等問題,讓手動拼接檔案路徑變得非常麻煩。

os.Root API 解決方案

無論使用者輸入什麼路徑,都只能存取指定 root 底下的檔案
root, err := os.OpenRoot("/app/uploads")
if err != nil {
log.Fatal(err)
}
defer root.Close()
f, err := root.Open("avatar/user-1.png")

無需額外套件如:safeopen🔗、沒有需要嚴謹驗證的路徑字串,單純定義一個 Root 路徑,之外的存取都是非法的。

總結

可以考慮所有 os.Open 的使用情境是否安全,並替換上 os.Root 確保「操作不應存取該目錄之外的檔案」。

// 可能跳脫 baseDirectory.
f, err := os.Open(filepath.Join(baseDirectory, filename))
// 永遠只會在 baseDirectory 內
f, err := os.OpenInRoot(baseDirectory, filename)

單次與多次操作可以使用 os.OpenInRootos.Root,差別在於 OpenInRoot🔗 其實就是 OpenRoot + r.Open + defer r.Close()

// 單次操作
f, err := os.OpenInRoot(baseDirectory, filename)
// 多次操作
root, err := os.OpenRoot(baseDirectory)

延伸閱讀