必须先用 http.MaxBytesReader 包装请求体,这是防恶意超大请求的唯一防线;需在 handler 开头设置总长限制(如 12MB),c.FormFile.Size 不可信,保存前须提取原始文件名、生成唯一名并确保目录存在。

必须先用 http.MaxBytesReader 包装请求体
这是最常被跳过的前置步骤,也是唯一能防恶意超大请求的防线。Fiber 没有全局上传限制钩子,c.FormFile 一调就触发 Go 原生 multipart 解析,此时请求体已开始读取——如果没提前限流,攻击者发个 10GB 的 Content-Length,服务可能卡死或 OOM。
-
http.MaxBytesReader必须放在 handler 最开头,且只对当前请求生效:r := c.Request() r.SetBodyStream(http.MaxBytesReader(c.Context(), r.Body, 12
- 这个值不是“单个文件大小”,而是整个
multipart/form-data请求体总长,要预留空间给title、desc等文本字段 -
c.FormFile("avatar").Size不可信——它来自 boundary 解析出的元数据,客户端可伪造;仅作业务层二次校验,不能替代流控
c.FormFile 和 c.MultipartForm().File 怎么选
取决于你是否需要多文件或混合表单字段。两者底层都依赖 ParseMultipartForm,但触发时机和返回结构不同。
- 单文件 + 简单场景 → 直接用
c.FormFile("file"):它会隐式调用ParseMultipartForm(32 并返回第一个匹配项 - 多文件(
)或需同时读文本字段 → 先显式调用c.ParseMultipartForm(32,再取c.MultipartForm().Value["title"]和c.MultipartForm().File["files"] - 注意:
c.MultipartForm()在未调用ParseMultipartForm前返回nil,直接访问会 panic 或静默失败
保存文件前必须做的三件事
绕过这三步,轻则路径遍历写入系统目录,重则磁盘写满、服务不可用。
- 用
filepath.Base(h.Filename)提取原始文件名,丢弃客户端传来的任何路径(如../../etc/passwd) - 生成服务端唯一文件名(如
uuid.NewString() + ".jpg"),绝不信任h.Filename - 检查目标目录是否存在:
os.MkdirAll(uploadDir, 0755);否则c.SaveFile或os.Create会报no such file or directory
c.SaveFile 的隐藏风险与替代方案
c.SaveFile 看似方便,但它底层仍会调用 file.Open() + io.Copy,而临时磁盘写入不受 MaxBytesReader 控制——也就是说,即使你设了 12MB 限制,如果解析时 maxMemory 设太小,Go 仍可能把超限部分悄悄写进 /tmp,撑爆磁盘。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 推荐手动控制:
file, err := h.Open() if err != nil { /* handle */ } defer file.Close() <p>dst, err := os.Create(filepath.Join(uploadDir, safeName)) if err != nil { /<em> handle </em>/ } defer dst.Close()</p><p>_, err = io.Copy(dst, file) // 流式写入,不占内存</p> - 避免
io.ReadAll(file)或bytes.Buffer全量读取,大文件必崩 - 若需支持断点续传或分块上传,
c.SaveFile完全不适用,得自己解析multipart.Reader或用第三方库
实际部署时,最容易被忽略的是 MaxBytesReader 的位置和 ParseMultipartForm 的阈值配合。设太高怕 OOM,设太低又频繁落盘;而一旦漏掉前者,后者再细也没用——因为恶意流量早就在解析前把连接拖死了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










