在gin中应优先用mime.typebyextension基于文件后缀做轻量拦截,再用魔数校验确保安全,禁用content-type依赖,严格管理文件句柄生命周期。

如何在 Gin 中通过 mime.TypeByExtension 校验文件后缀
直接靠文件名后缀做初步拦截最轻量,但不防伪造——适合前端已做基础限制、后端需快速兜底的场景。Gin 本身不提供上传类型白名单机制,得手动读取 multipart.FileHeader 的 Filename,再用标准库 mime.TypeByExtension 推断 MIME 类型。
注意:mime.TypeByExtension 只查扩展名映射表(如 .jpg → image/jpeg),不解析文件内容,所以它快但不可信。别把它当安全防线,只作第一道过滤。
- 先调用
file.Filename获取原始文件名,用path.Ext提取后缀(如.pdf) - 传给
mime.TypeByExtension(ext),返回类似application/pdf的字符串 - 和你允许的 MIME 列表比对,不匹配就
c.AbortWithStatusJSON(400, gin.H{"error": "unsupported file type"}) - 别忘了处理空后缀或非法字符:
ext == "" || strings.HasPrefix(ext, ".") == false都应拒绝
为什么不能只依赖 file.Header.Get("Content-Type")
浏览器上传时,Content-Type 是前端 JS 或表单自动设置的,完全可被篡改。攻击者用 curl 发个 -F "file=@evil.exe;type=image/png" 就能绕过所有基于 Header 的检查。
实测中,Chrome 对图片拖拽上传会填真实 MIME,但用户改请求头、换工具、甚至禁用 JS 后,这个字段就彻底失效。所以 Gin 路由里看到的 file.Header.Get("Content-Type") 只能当参考,绝不能作为唯一判断依据。
- 它来自 HTTP 请求体边界中的
Content-Type字段,非服务端解析结果 - 即使你校验了该值,也必须配合后缀推断或魔数检测才能降低风险
- 某些老旧客户端可能根本不发这个字段,导致空字符串,直接
== ""判断会误杀合法文件
用文件魔数(magic bytes)做二次校验
真正可靠的格式识别得看文件开头几个字节。比如 PNG 必须以 89 50 4E 47 开头,PDF 以 25 50 44 46(即 %PDF)开头。Gin 拿到 multipart.File 后,可以用 io.ReadFull 读前 4–8 字节比对。
这步要小心:读完之后文件指针偏移了,后续 file.Save() 或 io.Copy 会从中间开始写,导致文件损坏。必须用 file.Open() 得到 *os.File,然后 f.Seek(0, io.SeekStart) 回退,或者用 bytes.NewReader(data) 把读出的字节和剩余内容拼起来。
- 推荐先
file.Open(),再f.Read(buf[:4]),校验完立刻f.Seek(0, 0) - 常见魔数表建议硬编码:PDF(
%PDF)、PNG(\x89PNG)、JPEG(\xFF\xD8\xFF)、ZIP(\x50\x4B\x03\x04) - 别用第三方 magic 库(如
gabriel-vasile/mimetype),它内部也是读魔数+后缀,徒增依赖且无本质提升
完整流程里最容易漏掉的清理动作
很多人写了校验逻辑,却忘了上传文件临时存在内存或磁盘上的生命周期管理。Gin 默认把小文件放内存(MaxMultipartMemory),大文件走临时文件,但无论哪种,只要没调 file.Open() 或 file.Save(),Gin 不会自动删临时文件。
更隐蔽的问题是:校验失败后,如果没显式关闭 file 对象,fd 可能泄露;若用了 file.Open() 又没 defer f.Close(),临时文件句柄一直占着,Linux 下可能触发 too many open files。
- 所有
file.Open()后必须配defer f.Close(),哪怕只是读几个字节 - 校验失败时,立即
return前确保file已关闭(file != nil时调file.Close()) - 如果用了
c.SaveUploadedFile(file, dst),它内部会关 file;但自己用io.Copy就得手动关











