http.request.header.get("content-type")不可靠,因浏览器上传时该字段由前端控制、可随意伪造;服务端必须基于文件前512字节内容(而非扩展名或头字段)做真实mime判定,推荐用net/http.detectcontenttype()或手动匹配magic number。

为什么 http.Request.Header.Get("Content-Type") 不可靠
浏览器上传文件时,Content-Type 由前端控制,可随意伪造,比如把恶意二进制改头为 text/plain 绕过校验。服务端必须基于文件内容做真实 MIME 判定,不能信请求头。
Go 标准库的 net/http 不提供内容检测能力,得靠自己读取文件头部字节匹配 Magic Number。
- 常见错误:直接用
mime.TypeByExtension(".jpg")—— 这只查扩展名,和文件内容无关 - 更危险的做法:仅检查
multipart.FileHeader.Header.Get("Content-Type"),这仍是前端可控字段 - 正确路径:从
multipart.File读取前 512 字节(HTTP 规范要求 MIME 检测最多看这么多),传给net/http.DetectContentType()或手动比对 Magic
用 net/http.DetectContentType() 的实际限制
net/http.DetectContentType() 是 Go 官方提供的轻量检测函数,但它只覆盖常见文本/图像/归档类型(如 image/jpeg、text/html、application/zip),对 PDF、Office 文档、音视频等支持有限——它不识别 application/pdf,返回 application/octet-stream;也不区分 .docx 和 .xlsx,一律判为 application/zip(因为它们本质是 ZIP)。
如果你的业务需要精确识别 PDF 或 Office 文件,就得自己补充 Magic 匹配逻辑。
- 调用前必须确保输入是至少 512 字节的
[]byte,少于该长度会降级为默认类型 - 它不校验文件完整性,只做头部匹配,无法防篡改或截断文件
- 对 UTF-8 BOM 敏感:
DetectContentType([]byte("\uFEFF"))会返回text/html,但普通可能被误判为text/plain
手动匹配 Magic Number 的关键点
真正可控的识别方式是读取文件开头若干字节,硬编码比对 Magic Number。例如 PDF 固定以 %PDF- 开头(ASCII),DOCX 是 ZIP 格式,前 4 字节为 \x50\x4B\x03\x04(PK header)。
注意:不要一次性读整个文件——上传流可能很大,且攻击者可能构造超长伪造头部。只读前 512 字节足够覆盖所有常见 Magic。
- 用
io.ReadFull(file, buf[:n])替代file.Read(buf),避免因 EOF 提前终止导致误判 - 对 ZIP 类型(DOCX/XLSX/PPTX),需进一步解压并检查内部
[Content_Types].xml路径,但生产环境慎用——解压有资源风险 - PDF 检测别只查
%PDF-,还要跳过可能的空白或注释行(RFC 3778 允许首行是注释),建议扫描前 1024 字节内首个非空白非注释行
在 multipart.FormFile 流程中安全插入检测
Go 的 multipart.File 是 io.Reader,一旦读取就不可回溯。所以必须在保存或处理前,先“窥探”头部字节,再把剩余内容交给后续逻辑。
标准做法是用 io.MultiReader 把已读的 buffer 和原 file 拼接,或者用 bytes.NewReader(buf) + 原 file 构成新 Reader。
- 别用
file.Seek(0, io.SeekStart)——multipart.File不一定支持 Seek,会 panic - 检测后若类型不符,立即关闭 file 并 return,防止后续逻辑继续读取造成资源浪费
- 如果业务允许,把检测逻辑封装成中间件:接收
http.Handler,包装Request.Body,统一拦截上传请求
最易忽略的是临时文件残留——用 file.Save() 后没做 MIME 校验,结果恶意文件已落地。检测必须发生在 Save() 或写入磁盘之前。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











