fh.filename不可信,因其直接来自客户端http请求头,可能为空、含路径穿越或乱码;必须用filepath.base()剥离路径并校验清洗后才可安全使用。

为什么 c.FormFile("file") 返回的 *multipart.FileHeader 里 Fh.Filename 可能是空或被篡改
浏览器上传时,Fh.Filename 来自 HTTP 请求头中的 Content-Disposition 字段,完全由客户端提供,不可信。常见现象包括:空字符串、含路径(如 ../../etc/passwd)、中文乱码(未按 RFC 5987 编码)、或伪造名称。Gin 不做自动清洗,直接读取原始值。
- 必须校验并规范化文件名,不能直接用于保存或响应
- 若前端用
fetch+FormData且未显式设置filename,部分浏览器(如 Safari)可能返回空字符串 - IE/Edge 旧版本可能使用本地绝对路径,需截取 basename
如何安全提取原始文件名并做基础清洗
先调用 c.FormFile("file") 获取文件头,再对 Fh.Filename 做最小必要处理:
- 用
filepath.Base()剥离路径,防目录遍历:filepath.Base(fh.Filename) - 用
strings.TrimLeftFunc()清除开头的.或空格,避免隐藏文件或空白名 - 若需保留中文,不建议用简单正则替换,应依赖
mime.WordDecoder解码 RFC 2231/RFC 5987 编码(常见于 Chrome/Firefox 的非 ASCII 文件名) - 示例片段:
fh, err := c.FormFile("file") if err != nil { c.AbortWithStatusJSON(400, gin.H{"error": "no file provided"}) return } name := filepath.Base(fh.Filename) name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '.' || unicode.IsSpace(r) }) if name == "" { name = "unnamed" }
当 Fh.Filename 为空时,有哪些可靠 fallback 方式
不能假设它一定有值。可结合以下策略补全:
- 检查
c.Request.MultipartForm是否已解析,然后读c.Request.MultipartForm.Value["filename"](前提是前端额外传同名文本字段) - 用
uuid.NewString()生成唯一 ID 作为基础名,再拼上从Fh.Header.Get("Content-Type")推断的扩展名(如image/jpeg → .jpg) - 若业务允许,强制要求前端在
FormData.append("file", blob, "real-name.jpg")中显式传第三个参数 —— 这是最可控的方式 - 注意:不要用
c.GetHeader("X-File-Name"),HTTP 标准头无此字段,属自定义,需前后端约定且易被绕过
保存文件时,为什么不能直接用原始 Fh.Filename 拼接路径
这是最常踩的坑。Gin 的 c.SaveUploadedFile(fh, dst) 不校验 dst 路径安全性,若 dst 含 ../ 或绝对路径,会写入任意位置(如覆盖 /etc/passwd)。
- 务必对清洗后的文件名再次用
filepath.Clean(),并检查是否仍含..或以/开头 - 保存路径应固定根目录(如
./uploads/),再拼接清洗后的名:filepath.Join("./uploads", safeName) - Linux 下注意文件系统对
\0、控制字符的限制;Windows 下避免AUX、CON等保留名 - 示例校验:
safeName := filepath.Clean(name) if strings.Contains(safeName, "..") || strings.HasPrefix(safeName, "/") { c.AbortWithStatusJSON(400, gin.H{"error": "invalid filename"}) return }
文件名信任边界就在你调用 filepath.Base() 和 filepath.Clean() 的那一行。多一步校验,少一个 CVE。











