前言
最近在撰写的 Go 后端使用到大量的本地文件访问,其中最需要被防范的问题之一就是「路径遍历」。而在 Go 1.24 新增了 os.Root API 简洁优雅地避免安全问题。
现有问题
假设要搜寻一个文件夹内的文件时,最简单的情况是攻击者可能通过 ../ 跳出原本预期的文件夹,进而读取服务器上的任意文件。
baseDirectory := "/app/uploads"filename := "../../../etc/passwd" // 不安全的路径
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("无效路径")}但错误的操作仍然可能存在安全问题,比如以下的检查仅仅少了一个 / 结尾(应该是 /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("无效路径")}
// filename = /app/uploads_secret/flag.txt除了繁琐且容易出错的验证之外,还有奇怪的符号链接(Symbolic Link)、TOCTOU(time-of-check to time-of-use)竞态条件、平台差异⋯⋯等问题,让手动拼接文件路径变得非常麻烦。
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 确保「操作不应访问该目录之外的文件」。
// 可能跳出 baseDirectoryf, 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)