c.SaveUploadedFile虽便捷但存在路径遍历、目录未创建、文件名不安全等风险;须前置os.MkdirAll确保目录存在,用securejoin.SecureJoin净化路径,以filepath.Base提取并重命名文件,避免直接拼接file.Filename。

c.SaveUploadedFile 是 Gin 提供的最直接方式,但直接用它保存到指定目录前,有几个关键点必须处理:目录是否存在、文件名是否安全、路径是否越界。跳过这些,轻则报错 open uploads/../../etc/passwd: no such file or directory,重则被路径遍历攻击写入任意位置。
确保目标目录存在且可写
Go 不会自动创建多级目录,c.SaveUploadedFile 内部调用 os.Create,只创建文件,不创建父目录。如果 "uploads/icons" 不存在,就会失败。
- 手动创建:在保存前加
os.MkdirAll("uploads/icons", 0755) - 或用第三方库如
securejoin.SecureJoin(推荐),它同时做路径净化和目录保障 - 注意权限:Linux 下确保运行 Gin 的用户对目标目录有
w权限,否则报permission denied
别直接用 file.Filename 作路径拼接
前端传来的 file.Filename 可能是 ../../../etc/shadow 或带空格、中文、控制字符的名称,直接拼进 os.Create 就是安全隐患。
- 用
path.Base提取纯文件名,再用正则或白名单过滤非法字符 - 更稳妥的做法是重命名:比如用
uuid.New().String() + filepath.Ext(header.Filename) - 务必配合
securejoin.SecureJoin("uploads", cleanName),它会拒绝任何试图跳出uploads/的路径
c.SaveUploadedFile 的实际行为和替代方案
这个方法本质是打开源文件流 → 创建目标文件 → io.Copy,但它隐藏了错误细节。遇到大文件或磁盘满时,你可能只看到 write /path/to/file: no space left on device,没法定位是读失败还是写失败。
- 如需精细控制(比如限速、进度回调、分块校验),应手动展开:
file.Open()→os.Create()→io.CopyN或自定义 buffer - 注意:每次
Open()后必须defer src.Close(),否则 fd 泄露 -
c.FormFile("upload")和c.Request.MultipartReader()不能混用,前者已消耗 body,后者会读空
最易被忽略的是 router.MaxMultipartMemory —— 它默认 32 MiB,但上传 100 MiB 文件时,Gin 会在解析阶段就把整个 body 加载进内存,直接 OOM。必须在初始化时显式设为合理值,比如 router.MaxMultipartMemory = 128 (128 MiB),并配合 <code>http.MaxHeaderBytes 防止 header 膨胀攻击。

















