filepath.Clean不能防路径穿越,必须先Clean再校验是否在授权根目录内,且需用filepath.EvalSymlinks解析符号链接并验证真实路径。

用 filepath.Clean 处理用户输入的路径前,必须先校验是否越界
用户传入的 ../../etc/passwd 这类路径,filepath.Clean 会把它变成 /etc/passwd —— 但它本身**不阻止越界**,只是规范化。直接拼接后打开文件,就等于把整个文件系统暴露了。
正确做法是:先 filepath.Clean,再检查结果是否仍在允许根目录之下:
root := "/var/www/uploads"
userPath := "../../etc/shadow"
cleanPath := filepath.Clean(userPath)
absPath := filepath.Join(root, cleanPath)
// 关键校验:cleanPath 不能含 "..",且 absPath 必须以 root 开头
if strings.Contains(cleanPath, "..") || !strings.HasPrefix(absPath, filepath.Clean(root)+string(filepath.Separator)) {
return nil, errors.New("invalid path: directory traversal detected")
}
-
filepath.Clean是必要但不充分的步骤,它只做标准化,不负责权限控制 - 仅靠
strings.HasPrefix不够健壮,需确保root也经过filepath.Clean,否则/var/www/uploads/..可能绕过 - Windows 下分隔符是
\,统一用filepath.Separator而非硬写"/"
用 os.Open 前务必调用 filepath.EvalSymlinks 防符号链接逃逸
攻击者可能上传一个指向 /etc 的符号链接,filepath.Clean 对它完全无效。如果只校验路径字符串,而没解析实际磁盘路径,就会被绕过。
必须在最终打开前确认真实路径是否仍在沙箱内:
立即学习“go语言免费学习笔记(深入)”;
absPath := filepath.Join(root, cleanPath)
realPath, err := filepath.EvalSymlinks(absPath)
if err != nil {
return nil, err
}
if !strings.HasPrefix(realPath, filepath.Clean(root)) {
return nil, errors.New("symlink escapes allowed directory")
}
f, err := os.Open(realPath) // 此时才安全
-
filepath.EvalSymlinks会递归解析所有符号链接,返回真实物理路径 - 注意它可能失败(如链接指向不存在路径),错误需显式处理,不能忽略
- 该调用有 syscall 开销,在高频小文件场景下需权衡;但安全场景下不可省略
别依赖 http.Dir 默认行为做路径过滤
http.Dir 确实内置了基础防护,比如对 .. 返回 404,但它**只作用于 HTTP handler 内部**,且不处理符号链接、空字节、Unicode 归一化等变体攻击。
- 它不会调用
filepath.EvalSymlinks,符号链接仍可逃逸 - 它的清理逻辑在
http.ServeFile中,若你手动用http.Dir.Open拿到fs.File,就绕过了所有防护 - 自定义静态服务或 ZIP 文件解压、模板读取等场景,
http.Dir完全不生效
Go 1.16+ 的 embed.FS 不受目录穿越影响,但仅限编译期静态资源
embed.FS 在编译时打包文件,运行时所有路径都是虚拟的,无法访问宿主机文件系统 —— 所以天然免疫目录穿越。但它只适用于已知、固定、编译期确定的资源。
- 不能用于用户上传文件、动态配置加载、日志读取等运行时路径场景
- 若误以为
//go:embed能保护任意路径,而在代码里拼接用户输入进embedFS.ReadFile,会 panic 或返回fs.ErrNotExist,但不代表安全 —— 这说明逻辑已错位 - 适合场景:前端静态资源、内置 SQL 模板、默认配置文件等
os.Stat 或 os.Open 之前不走一遍 filepath.EvalSymlinks,就等于留了一扇虚掩的门。


















