应以文件头魔数校验真实类型,优先用http.detectcontenttype初筛前512字节,再按白名单严格匹配;对png、jpeg、pdf等关键类型须手动校验魔数,禁用content-type头和字符串前缀判断。

只信文件头,别信Content-Type头
浏览器或Postman伪造 Content-Type 太容易了,c.Request.MultipartForm() 或 c.FormFile() 返回的 file.Header.Get("Content-Type") 完全不可靠。真正有效的校验必须读取文件前几百字节,用 magic bytes 匹配真实类型。
用http.DetectContentType做快速初筛
http.DetectContentType 内置了常见格式(PNG、JPEG、PDF、ZIP等)的 magic 判断逻辑,只需读前 512 字节即可。它不完美但够用,且比手写 byte slice 比对更易维护。
- 调用
file.Open()后,用io.ReadFull(src, head[:])读取前 512 字节(注意检查err == io.ErrUnexpectedEOF) - 传入
http.DetectContentType(head[:]),得到类似"image/png"的字符串 - 白名单硬编码比正则更安全:
allowed := map[string]bool{"image/png": true, "image/jpeg": true, "application/pdf": true} - 别用
strings.HasPrefix判断"image/"—— 攻击者可构造image/xxx\0shell.php绕过
对关键类型手动校验 magic bytes 更稳妥
比如 PNG 必须以 0x89 0x50 0x4E 0x47 开头,JPEG 是 0xFF 0xD8,PDF 是 %PDF(ASCII)。这些比 http.DetectContentType 更难绕过,适合高安全要求场景。
- 先确保已读取足够字节(如 PNG 至少要 4 字节,PDF 至少 4 字节)
- 用
bytes.Equal(header[:4], []byte{0x89, 0x50, 0x4E, 0x47})显式比对 - PDF 要注意 ASCII 字符比较:
bytes.HasPrefix(header[:], []byte("%PDF")) - 如果文件太小(比如
len(header) ),直接拒绝,避免 panic 或误判
别在读完 header 后还让 file.Body 继续被其他逻辑消费
调用 file.Open() 得到的 src 是一个 io.ReadSeeker,但多数情况下你只读了前 N 字节,后续保存或哈希时需要从开头重读 —— 必须 src.Seek(0, 0),否则会从中间开始写入,文件损坏。
- 没
Seek能力的 reader(比如某些代理后端)需用io.MultiReader(bytes.NewReader(header[:]), src)拼接 - 更简单做法:校验完立刻
src.Close(),再重新file.Open()用于保存,虽然多一次 open,但语义清晰、不易出错 - 如果用了
c.SaveUploadedFile(),它内部会自己 reopen,但你无法控制是否 seek —— 所以校验逻辑必须在SaveUploadedFile之前完成,且不能依赖同一个src实例











