gin.context.file 直接拼接用户输入路径存在严重路径遍历风险,必须手动执行四层防护:url解码、filepath.clean、绝对路径白名单校验、os.lstat防符号链接,且不可依赖filepath.islocal。

为什么 gin.Context.File 直接用用户输入路径等于裸奔
它不校验、不 Clean、不检查前缀,也不防符号链接。只要攻击者传个 ../../etc/passwd 进来,ctx.File("/uploads/" + filename) 就会直接读取系统文件。Gin 的 File 和 FileAttachment 都是纯 IO 封装,和 http.ServeFile 一样,没加任何安全层。
必须手动做四层路径净化:解码 → Clean → 白名单前缀 → Lstat 防竞态
用户输入的路径字段(比如 query 中的 file、JSON body 里的 path)不会被 Gin 自动 URL 解码;r.URL.Path 才会,但 query 参数不会。所以第一步永远是:url.PathUnescape —— 只调一次,别重复。
- 解码后立刻传给
filepath.Join(cleanRoot, unescaped),别字符串拼接(如"/data/" + raw) -
filepath.Clean后必须用绝对路径白名单校验:cleanRoot := filepath.Clean("/var/www/uploads") + string(filepath.Separator),再用strings.HasPrefix(cleanPath, cleanRoot) - 校验通过后,打开前必须
os.Lstat:如果info.Mode()&os.ModeSymlink != 0,直接拒绝 - Windows 下建议先
filepath.ToSlash统一斜杠,避免\..\绕过
别信 filepath.IsLocal,它只是轻量过滤
filepath.IsLocal(Go 1.20+)只判断路径是否“看起来本地”,比如不含 :// 或驱动器盘符,但它完全不处理 ../、不校验范围、不防符号链接。用它替代白名单前缀校验,等于锁了门却把钥匙留在地上。
用 http.ServeContent 替代 ctx.File 更可控
当你需要自定义响应头、支持断点续传或精细控制读取逻辑时,http.ServeContent 比 ctx.File 更合适。但注意:它仍不自带路径校验,你得自己把净化后的 *os.File 和 os.Stat 结果传进去。关键点是——文件句柄必须来自你已通过四层校验的路径,不能跳过 Lstat 直接 os.Open。
os.Lstat 返回到 os.Open 之间,攻击者可能把目标文件替换成软链。没有 os.O_NOFOLLOW(Linux/macOS)或等效防护,这层就形同虚设。











