应使用中间件在调用c.FormFile()前校验:先filepath.Clean()规范化文件名,拦截含..或绝对路径的非法路径;再用正则^[a-zA-Z0-9._-]+$校验字符合法性;禁用.php/.jsp等危险后缀;图片上传需校验magic bytes;预读并重放multipart body以实现前置拦截。

怎么用中间件拦截非法文件名和路径穿越
文件上传的非法行为往往始于文件名本身:比如 ../../etc/passwd、shell.php\x00.jpg 或带控制符的 photo\x1b.png。Gin 的 c.FormFile() 不做任何校验,直接返回 *multipart.FileHeader,而 file.Filename 是原始字符串——它可能已被前端恶意构造。必须在调用 c.FormFile() 前或刚拿到后立即拦截。
关键动作是「路径规范化 + 白名单比对」:
- 用
filepath.Clean(file.Filename)消除../、./、重复斜杠等绕过手法;结果若仍含..或开头为/,说明存在路径穿越风险 - 检查清理后名字是否只含安全字符:
^[a-zA-Z0-9._-]+$(注意:不建议用正则全量扫描,但用于文件名基础校验可接受) - 禁止常见危险后缀:
.php、.jsp、.sh、.env,用strings.HasSuffix()快速判断 - 若业务只允许图片,额外检查
file.Header的 magic bytes(如前 4 字节是否为\xff\xd8\xff),而非只信后缀
怎么在中间件里提前读取并校验 multipart body
Gin 默认把 multipart 解析延迟到首次调用 c.FormFile() 或 c.MultipartForm(),这意味着你无法在中间件里靠“看请求体”来拦截——因为此时 body 还没被读。必须手动预读一次,并重放给后续逻辑。
典型做法是:在中间件中用 io.ReadAll(c.Request.Body) 获取原始字节,扫描其中是否含非法控制符(如 \x00、\x1f)或可疑 payload(如 ),再用 bytes.NewReader() 和 io.NopCloser() 把 body 重置回 c.Request.Body。
但要注意:
- 只对小文件体做此操作(建议上限 2MB),大文件直接
c.AbortWithStatus(413)拒绝,避免内存暴涨 - 不要在中间件里调用
c.MultipartForm()—— 它会触发完整解析,且不可逆,后续c.FormFile()可能返回 nil - 若已知 Content-Type 是
multipart/form-data,且 header 中Content-Length超限,可跳过读 body,直接拦截
为什么不能只依赖 SaveUploadedFile 的路径参数校验
c.SaveUploadedFile(file, dst) 的 dst 参数看似可控,但它只负责写入,不负责校验 file.Filename 是否安全。你传 "./uploads/" + file.Filename,攻击者传 ../../.ssh/id_rsa,Clean 后变成 /.ssh/id_rsa,最终写入根目录——SaveUploadedFile 完全不会报错。
更隐蔽的问题是:如果前端用 JS 动态拼接 FormData,可能注入空字节 \x00 截断字符串,导致系统调用提前终止,绕过你的校验逻辑。所以必须在拿到 file.Filename 的**第一时刻**就做 filepath.Clean 和控制符扫描,而不是等到保存时才处理。
常见疏漏点:
- 只校验主文件字段(如
file),忽略同请求中的其他表单字段(如title、desc)也可能含控制符,影响日志或数据库 - 多文件上传时,循环调用
c.FormFile("files"),但只对第一个文件做校验,后面全放行 - 用
strings.Contains(file.Filename, "..")替代filepath.Clean,会被....//或 URL 编码绕过
如何让拦截不破坏 Gin 的 multipart 流程
核心原则:中间件里别动 c.Request.MultipartForm,也别调用 c.MultipartForm();所有校验必须基于原始 c.Request.Body 字节或 c.FormFile() 返回的 file 结构体字段。
正确链路是:
- 中间件 A:读 body → 扫描控制符 → 拦截或重放
- 中间件 B:调用
c.FormFile("xxx")→ 立即filepath.Clean(file.Filename)→ 校验后缀/长度/magic → 不通过则c.AbortWithStatus(400) - Handler:只用已校验过的
file调用file.Open()+io.Copy(),绕过SaveUploadedFile
最后提醒:Gin 的 MaxMultipartMemory 只控制内存缓冲大小,不影响磁盘临时文件生成。攻击者仍可通过大文件耗尽磁盘空间,所以业务层必须有独立的大小限制(如 file.Size ),不能只信框架配置。











