前言
最近在撰寫的 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.OpenInRoot 或 os.Root,差別在於 OpenInRoot 其實就是 OpenRoot + r.Open + defer r.Close()。
// 單次操作f, err := os.OpenInRoot(baseDirectory, filename)
// 多次操作root, err := os.OpenRoot(baseDirectory)