http.detectcontenttype不可靠,会漏判jpeg、pdf、zip等文件:它仅读前512字节且依赖固定magic bytes位置,对截断、混淆、伪装文件易返回application/octet-stream或text/plain;必须结合专用库二次校验,并严格校验文件头与扩展名一致性。

只信 http.DetectContentType 会漏掉 JPEG、PDF 和 ZIP 文件
它只读前 512 字节,且依赖固定 magic bytes 位置。比如一个被截断的 JPEG(开头只有 0xff, 0xd8),http.DetectContentType 很可能返回 "application/octet-stream" 而不是 "image/jpeg";PDF 文件若头部被混淆或含 BOM,也可能识别失败;ZIP 及其变体(如 DOCX、JAR)更难靠这 512 字节准确判断。
实际场景中,用户上传的“图片”可能是伪装成 PNG 的恶意脚本,而 http.DetectContentType 对这种头部篡改无能为力。
- 别把
http.DetectContentType当最终判决——它只是初筛工具 - 对图像、PDF、Office 文档、归档包等关键类型,必须用专用库(如
github.com/h2non/filetype或golang.org/x/image)做二次校验 - 读取 buffer 时务必用
io.ReadFull,避免file.Read(buf)只读几字节就返回,导致探测数据不全 - 空文件或小于 4 字节的文件,
http.DetectContentType固定返回"text/plain; charset=utf-8",需额外检查header.Size == 0
r.FormFile 拿到的 Content-Type 头完全不可信
浏览器或客户端可随意设置 multipart part 的 Content-Type,比如传一个 PHP WebShell 却声明 Content-Type: image/gif,后端若只比对白名单就会放行。
真实攻击案例里,常见 payload 是 shell.php.jpg(扩展名合法)+ Content-Type: image/jpeg(头也伪造),但文件内容是 PHP 代码。
- 永远不要用
header.Header.Get("Content-Type")做唯一依据 - 必须结合文件头探测结果 + 扩展名逻辑一致性校验:比如探测出
"image/png",但原始名是payload.php或hack.exe,直接拒绝 - 白名单应按 MIME 类型定义,而非扩展名——
"image/png"允许.png,但不允许.jpg,哪怕它确实是 JPEG 内容(这是策略问题,不是技术限制)
安全重命名必须丢弃原始 Filename,且路径要防逃逸
fileHeader.Filename 是客户端可控字段,含 ../、空字节、Unicode 控制符等风险。直接拼进 os.Create 路径等于开放任意写权限。
典型错误:dst, _ := os.Create("./uploads/" + fileHeader.Filename) —— 攻击者传 ../../etc/passwd 就能覆盖系统文件。
- 彻底丢弃
fileHeader.Filename,生成服务端唯一 ID:uuid.New().String()或crypto/rand.Read(16)转 hex - 扩展名只从 MIME 白名单映射获取,例如
"image/jpeg" → ".jpg",绝不信任filepath.Ext(fileHeader.Filename) - 拼接路径后必须调用
filepath.Clean(),再用strings.HasPrefix(absPath, absUploadDir)确认没逃逸出目标目录 - 写入前检查父目录是否存在、是否为符号链接、是否可写,防止 symlink race
大文件上传必须流式处理,否则 OOM 或磁盘打满
r.ParseMultipartForm(maxMemory) 默认把整个请求体加载进内存或临时磁盘。攻击者上传 10GB 文件,服务可能直接崩溃。
标准库不提供总上传体积控制,MaxMemory 只控内存部分,磁盘缓存仍可能无限增长。
- 优先用
r.MultipartReader()替代ParseMultipartForm,逐 part 流式读取 - 对每个
*multipart.Part,用io.LimitReader(part, maxFileSize)限制单文件大小(如 10MB) - 用原子计数器累加已读字节数,超总限制(如 100MB)立即
http.Error(w, "", http.StatusRequestEntityTooLarge)并关闭连接 - 禁用默认磁盘缓存,手动指定
tempDir并设权限(如0700),避免临时文件泄露
file.Read(buffer) 把读取位置移到了 512 字节后,后续 io.Copy 就会漏掉开头内容。必须在探测完调用 file.Seek(0, 0),而且得确认返回 err 为 nil。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











