必须在解析前用BodyLimit拦截超大请求,再通过MultipartReader预检字段名和文件名;c.FormFile()会触发完整解析,不可用于拦截。

怎么用中间件拦截超大或恶意文件上传
直接在 handler 里调 c.FormFile() 已经太晚——请求体可能早已全部读入内存,甚至触发 OOM。必须在 body 解析前就掐断非法上传。Echo 自带的 middleware.BodyLimit("5MB") 是第一道防线,但它只限制总请求体大小,不区分是 JSON 还是 multipart。
常见错误是只校验 Content-Length 头:攻击者可伪造该 header,而真正解析时才发现远超预期。正确做法是组合使用:
- 前置
BodyLimit拦截明显过大的请求(如 >100MB) - 在自定义中间件中检查
c.Request().Header.Get("Content-Type")是否含multipart/form-data - 若匹配,再调
c.Request().MultipartReader()尝试读取第一个 part 的 header,提取Content-Disposition中的filename和name字段做白名单校验(如只允许avatar或document字段) - 拒绝
filename含../、空格、控制字符或扩展名不在["jpg", "png", "pdf"]中的请求
为什么 c.FormFile() 不能用作拦截点
c.FormFile("file") 内部会调用 ParseMultipartForm,这会把整个 multipart body 缓存到内存或临时磁盘;一旦调用,就代表你已接受该上传请求。它不是“检查”,而是“消费”。
所以拦截必须发生在它之前。如果你在中间件里写了:
if strings.Contains(c.Request().Header.Get("Content-Type"), "multipart/form-data") {
_, err := c.FormFile("file") // ❌ 错!这里已经触发解析
if err != nil { /* ... */ }
}
那等于主动帮攻击者完成了一半上传。正确姿势是用 multipart.NewReader() 手动 peek 第一个 part,不读取 body 数据流本身。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
如何安全提取并校验上传字段名和文件名
攻击者常通过改写 name 或 filename 绕过前端限制。比如把 name="avatar" 改成 name="xss_payload",或 filename="shell.php" 伪装成图片。
实操要点:
- 用
mr, err := c.Request().MultipartReader()获取 reader,再part, err := mr.NextPart() - 从
part.Header.Get("Content-Disposition")解析出name和filename(推荐用mime.ParseMediaType,别手撕字符串) - 对
name做严格白名单:if name != "avatar" && name != "cover" - 对
filename先做filepath.Base()防路径穿越,再取扩展名:ext := strings.ToLower(filepath.Ext(filename)),再比对预设列表 - 校验完立刻
return c.NoContent(400)+return,避免后续 handler 执行
要不要在中间件里读取文件内容做病毒扫描
不要。中间件不是业务逻辑层,也不该承担 I/O 密集型任务。AV 扫描应交给异步队列(如 RabbitMQ + ClamAV worker),中间件只做元数据拦截。
更现实的边界是:
- 拦截空文件(
part.Size == 0) - 拦截超长文件名(>255 字节)
- 拦截
Content-Transfer-Encoding: base64且解码后 size 突增的 case(防膨胀攻击) - 记录原始
Content-Length和实际读取的 part size,发现差异过大时告警(可能被篡改或分块异常)
真正的文件内容校验和存储,留给 handler 里的 c.SaveUploadedFile() 之后做,那时你已确认字段合法、用户有权限、目录可写——条件才完整。

















