parsemultipartform 的 maxmemory 参数控制整个 multipart 请求体在内存中缓存的总字节数,包括所有文本字段值、文件元数据及部分文件内容,超限部分自动落盘,并非仅限文本字段或单个文件大小。

ParseMultipartForm 的 maxMemory 参数到底控制什么
它只管 form 字段值(比如文本输入框内容)的内存缓存上限,不是文件大小限制。设为 32 意味着 ≤32MB 的文本字段留在内存,超限部分写入临时磁盘;但上传的文件本身——哪怕 500MB——仍可能被 <code>r.FormFile 返回一个指向磁盘临时文件的句柄,不直接吃内存。
常见错误是以为设了这个就万事大吉,结果攻击者传一个 2GB 的 file 字段 + 1KB 文本字段,ParseMultipartForm(32 成功返回,后续却因没校验 <code>header.Size 导致文件被完整落盘到 os.TempDir(),填满磁盘。
- 必须在调用
r.FormFile后立刻检查fileHeader.Size是否超过业务阈值(如 10MB) - 若只想要流式处理、完全避开磁盘临时文件,就别调
ParseMultipartForm,改用r.MultipartReader() - maxMemory 设为 0 并不等于“不限”,而是触发 Go 默认行为(约 32MB),必须显式赋值
http.MaxBytesReader 是最后一条防线,但必须放对位置
它包装的是 r.Body,作用于整个 HTTP 请求体(含 headers + multipart boundary + 所有字段),字节级截断。但它只在 Read() 时生效,如果放在 ParseMultipartForm 之后,multipart 解析器可能已经把恶意数据读进内存或磁盘。
典型坑:有人把 MaxBytesReader 放在 handler 末尾做“兜底”,实际完全无效;还有人误包 w(响应体)而非 r.Body,导致毫无防护效果。
- 必须在 handler 开头第一行执行:
r.Body = http.MaxBytesReader(w, r.Body, 10 - 它的值应略大于你允许的最大文件 + 预估的文本字段总长(比如文件上限 10MB,加 100KB 表单字段,设 10.1MB)
- chunked 编码请求下
r.ContentLength为 -1,不能用来做前置判断,必须依赖MaxBytesReader
为什么不能只靠内存限制,还得手动流式校验文件大小
ParseMultipartForm 和 MaxBytesReader 都无法精确控制单个文件字段的体积。前者不管文件字段长度,后者管总长但不识别 multipart 结构。攻击者可以构造多个小文件字段绕过总长限制,或在一个大文件里混入大量无用 boundary 数据拉高总长却不增加实际 payload。
真正可靠的文件大小控制,只能靠手动读取 + 计数:
- 用
r.MultipartReader()获取 reader,遍历每个part - 遇到
form-data; name="file"的 part 时,用io.CopyN(ioutil.Discard, part, limit+1)或逐块读取并累加字节数 - 一旦累计 > 业务上限(如 10MB),立即
http.Error(w, "...", http.StatusRequestEntityTooLarge)并 return - 这样能确保即使 multipart 结构被畸形构造,也不会让服务端多分配一 KB 内存或磁盘
临时文件不清理会悄悄拖垮服务器
Go 的 ParseMultipartForm 在 maxMemory 超限时自动把文件写到 os.TempDir(),但框架**不会自动清理**。如果 handler panic、提前 return、或忘记 defer file.Close(),这些临时文件就永久滞留。
Linux 系统的 /tmp 通常有自动清理策略,但周期可能是数天;容器环境里 TempDir() 往往映射到持久卷,垃圾文件越积越多。
- 每次成功调用
r.FormFile后,拿到的*os.File句柄对应的就是那个临时磁盘文件 - 务必在 handler 结束前显式
os.Remove(file.Name()),且要检查err - 更稳妥的做法:用
os.CreateTemp自建临时目录 + 原子写入,上传完成后os.Rename到目标路径,再删源文件 - 不要依赖
defer os.Remove(...)—— 如果 handler 中途 panic,defer 可能不执行
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











