必须在保存文件前完成全部校验:用detectcontenttype检测真实mime类型并匹配白名单,校验扩展名逻辑一致性,限制文件大小与总量,安全重命名并校验路径,最后做格式特异性内容扫描。

上传前必须检查 Content-Type 和文件扩展名是否匹配
仅靠前端传来的 filename 或后端 mime.TypeByExtension 推断类型不可信。攻击者可伪造扩展名(如 shell.php.jpg)绕过校验,或直接修改 Content-Type 为 image/jpeg 但实际内容是 PHP 代码。
正确做法是:先用 http.Request.ParseMultipartForm 解析,再对 *multipart.FileHeader 调用 Open() 获取 io.ReadCloser,读取前 512 字节传给 net/http.DetectContentType —— 这个函数基于 magic bytes 判定真实 MIME 类型。
- 只允许白名单 MIME 类型,例如:
"image/jpeg"、"image/png"、"application/pdf" - 同时比对扩展名是否与 MIME 类型逻辑一致(如
image/jpeg对应.jpg或.jpeg),不一致则拒绝 - 注意
DetectContentType对空文件或极小文件可能返回text/plain,需额外判断文件大小是否为 0
限制单个文件大小和总上传体积,避免内存/磁盘耗尽
Go 默认的 ParseMultipartForm 会把整个 multipart body 加载进内存或临时磁盘,若不限制,攻击者可上传超大文件触发 OOM 或填满磁盘。
必须在调用 r.ParseMultipartForm 前设置 MaxMemory,并手动控制磁盘缓存路径和总大小:
- 调用
r.MultipartReader()替代ParseMultipartForm,逐 part 流式处理,避免一次性加载全部 - 对每个
*multipart.Part,用io.LimitReader(part, maxFileSize)控制单文件上限(如 10MB) - 用计数器累加已读字节数,超过总限制(如 100MB)立即关闭连接并返回
http.StatusRequestEntityTooLarge - 临时文件写入前确保目录存在且权限受限,避免写到系统关键路径
保存文件时必须重命名,禁用原始 Filename
fileHeader.Filename 完全由客户端控制,含路径遍历(../../etc/passwd)、空字节、特殊字符等风险。直接拼接会导致任意文件写入或覆盖。
安全做法是彻底丢弃原始名,生成服务端可控的文件 ID:
- 用
crypto/rand.Read生成 16 字节随机数,转成 hex 或 base32 作为主文件名 - 保留扩展名时,仅从白名单 MIME 映射中获取(如
"image/png" → ".png"),绝不信任原始扩展名 - 完整路径用
path.Join(uploadDir, safeName+ext)构造,并在写入前调用filepath.Clean+strings.HasPrefix校验是否仍在目标目录内 - 写入前检查目标路径是否存在、是否为符号链接、父目录是否可写——防止竞态条件下的 symlink race
上传后建议做轻量级内容扫描,防“合法格式包裹恶意载荷”
即使 MIME 和扩展名都合规,某些格式(如 PDF、Office 文档、SVG)仍可能嵌入脚本或远程资源引用。生产环境建议增加一层内容解析校验:
- PDF:用
github.com/unidoc/unipdf/v3/model检查是否含 JavaScript action 或非法 URI - SVG:用
golang.org/x/net/html解析 DOM,拒绝含<script></script>、onload、href="javascript:"的节点 - 图片:用
image.DecodeConfig验证宽高是否合理(防超大尺寸导致解码崩溃),并跳过含 ICC profile 的异常元数据 - 所有扫描必须设超时(如 3 秒)和内存限制,失败则拒绝文件,不回退到“信任原始 MIME”逻辑
最易被忽略的是:校验逻辑必须在保存文件**之前**完成。一旦落盘再扫描,就存在时间窗口被利用。另外,所有 IO 操作(包括临时文件创建、磁盘写入、外部库解析)都要有 panic 捕获和资源清理,否则可能留下半截文件或 fd 泄漏。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











