上传文件会覆盖同名文件,因c.saveuploadedfile()直接调用os.create()(含o_trunc标志),默认静默覆盖;需在保存前通过时间戳重命名、uuid或存在性检查确保路径唯一,并用filepath.base和正则清洗filename防路径遍历。

上传文件时为什么会覆盖已存在文件
因为 c.SaveUploadedFile() 默认不做文件存在性检查,直接用目标路径写入——如果 dst 路径已有同名文件,就会被静默覆盖。这不是 Gin 的 bug,而是 Go 标准库 os.Create() 的默认行为(等价于 os.O_CREATE | os.O_WRONLY | os.O_TRUNC)。
避免覆盖的三种实用策略
核心思路是:在调用 c.SaveUploadedFile() 前,确保目标路径唯一。常用做法有:
- 用时间戳 + 随机字符串重命名,如
fmt.Sprintf("%d_%s", time.Now().UnixNano(), file.Filename) - 用 UUID 生成新文件名,保留原始扩展名:
uuid.New().String() + filepath.Ext(file.Filename) - 先检查文件是否存在,存在则递增后缀(如
report(1).pdf),但需注意并发场景下竞态风险
推荐第一种:简单、高效、无竞态,且能天然规避中文名、特殊字符导致的路径问题。
c.FormFile() 返回的 file.Filename 不可信
前端可任意伪造该字段,比如传 ../../etc/passwd 或空字符串。直接拼接进路径会引发路径遍历或写入失败。必须做两件事:
- 用
filepath.Base(file.Filename)提取纯文件名,剥离所有路径信息 - 用
path.Clean()或正则过滤掉非法字符(如/ \ : * ? " |)
否则即使加了时间戳,攻击者仍可能通过构造恶意 Filename 绕过重命名逻辑,触发覆盖或越权写入。
完整安全保存示例(含防覆盖+防路径遍历)
file, err := c.FormFile("file")
if err != nil {
c.String(http.StatusBadRequest, "获取文件失败: %v", err)
return
}
// 1. 清洗原始文件名
cleanName := filepath.Base(file.Filename)
cleanName = regexp.MustCompile(`[^\w.\- ]+`).ReplaceAllString(cleanName, "_")
// 2. 生成唯一文件名
uniqueName := fmt.Sprintf("%d_%s", time.Now().UnixNano(), cleanName)
dst := filepath.Join("uploads", uniqueName)
<p>// 3. 确保 uploads 目录存在
if err := os.MkdirAll("uploads", 0755); err != nil {
c.String(http.StatusInternalServerError, "创建目录失败: %v", err)
return
}</p><p>if err := c.SaveUploadedFile(file, dst); err != nil {
c.String(http.StatusInternalServerError, "保存文件失败: %v", err)
return
}
c.String(http.StatusOK, "文件已保存为: %s", uniqueName)
</p>
真正容易被忽略的是:filepath.Join() 必须显式调用,不能靠字符串拼接;os.MkdirAll() 必须在 SaveUploadedFile() 前执行;而清洗逻辑一旦漏掉,重命名就形同虚设。











