必须在解析文件后、保存前做后缀校验,仅靠前端或content-type检查不可靠;需用path.ext()提取小写后缀并比对显式白名单,同时防范路径遍历和空后缀,并推荐结合二进制头检测。

必须在解析文件后、保存前做后缀校验,仅靠前端或 Content-Type 检查完全不可靠。
为什么不能只依赖 Content-Type 判断文件类型
浏览器或 curl 提交时,Content-Type 是由客户端自行设置的,攻击者可轻易伪造为 image/png,而实际上传的是 .php 或带 shell 的 .jpg.php。Gin 的 c.FormFile() 只读取表单字段,不校验二进制内容,所以仅靠 header 里的 Content-Type 过滤等于形同虚设。
-
c.Request.Header.Get("Content-Type")返回的是整个 multipart 请求的 boundary 类型(如multipart/form-data; boundary=xxx),不是单个文件的 MIME - 即使你从
file.Header.Header.Get("Content-Type")读到值,它也只是客户端声明,未经过任何验证 - 常见绕过方式:上传
shell.jpg,但文件头是 PHP 内容;或用test.php.jpeg混淆后缀
c.FormFile() 后立即提取并校验后缀
拿到 multipart.FileHeader 后,用 path.Ext() 提取扩展名,并统一转小写比对白名单。这是最轻量、最有效的第一道防线。
- 必须调用
strings.ToLower(path.Ext(header.Filename)),避免大小写绕过(如.JPG) - 白名单应显式列出,不要用黑名单(比如“不允许 .php”——漏掉 .phtml/.phar 就危险)
- 注意:
path.Ext()对archive.tar.gz返回.gz,不是.tar.gz;若需支持多级后缀,得自己实现解析逻辑 - 示例安全校验:
file, header, err := c.FormFile("avatar")
if err != nil {
c.JSON(400, gin.H{"error": "no file provided"})
return
}
ext := strings.ToLower(path.Ext(header.Filename))
allowed := map[string]bool{".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".webp": true}
if !allowed[ext] {
c.JSON(400, gin.H{"error": "file extension not allowed"})
return
}
防止路径遍历和空后缀导致的覆盖风险
用户可能传 ../../etc/passwd 或 shell.php.(末尾点号),直接拼接路径会引发严重问题。Gin 不自动清理文件名,必须手动处理。
- 永远不要直接使用
header.Filename构造保存路径 - 用
securejoin.SecureJoin("uploads", cleanName)(推荐)或至少用filepath.Base()截取纯文件名 - 若后缀为空(
path.Ext("script")返回""),应拒绝——这可能是故意去掉后缀绕过校验 - 生成新文件名时,优先用哈希 + 时间戳 + 原后缀,而非原名,例如:
fmt.Sprintf("%s%s", md5sum, ext)
额外建议:用 mimesniffer 做二进制头校验(可选但强烈推荐)
如果业务对安全性要求高(如用户头像公开访问),应在保存前读取文件前几百字节,用 net/http.DetectContentType() 或 github.com/gabriel-vasile/mimetype 检查真实 MIME 类型是否匹配后缀。
- 调用
c.FormFile()后得到的是FileHeader,它不含文件内容;需用file.Open()打开并读取前 512 字节 - 注意:
c.SaveUploadedFile()是流式保存,不加载全文到内存;但头检测需额外打开一次文件句柄 - 示例片段:
f, _ := file.Open()
defer f.Close()
buf := make([]byte, 512)
_, _ = f.Read(buf)
typ := http.DetectContentType(buf)
if !strings.HasPrefix(typ, "image/") {
c.JSON(400, gin.H{"error": "invalid image content"})
return
}
真正容易被忽略的不是“怎么写校验”,而是忘了校验必须发生在 SaveUploadedFile 之前,且必须结合后缀 + 内容双重检查——否则任何一层缺失都可能让木马图片落地执行。











