不能直接用原始文件名,因其可能含路径穿越、中文、空格或恶意字符,导致安全风险或404;应使用 uuid.NewString() + filepath.Ext(fh.Filename)(无扩展名时fallback为".bin")生成安全文件名。

为什么不能直接用原始文件名
用户上传的 Filename 可能含路径(如 ../../etc/passwd)、中文、空格、特殊符号,甚至恶意构造。Gin 的 c.FormFile() 虽会做基础校验,但不处理文件名语义安全。直接保存会导致覆盖、路径穿越或 404(Nginx 对空格/编码处理不一致)。
推荐用 uuid.NewString() + 扩展名组合
Go 标准库无内置 UUID,需引入 github.com/google/uuid。关键点是:只取原始文件的扩展名,不信任任何其他部分。
- 先调用
c.FormFile("file")获取*multipart.FileHeader - 用
filepath.Ext(fh.Filename)提取扩展名(自动处理.tar.gz等多级后缀) - 生成新名:
uuid.NewString() + filepath.Ext(fh.Filename) - 注意:
filepath.Ext()对无扩展名文件返回空字符串,建议 fallback 为".bin"
fh, err := c.FormFile("file")
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": "no file received"})
return
}
ext := filepath.Ext(fh.Filename)
if ext == "" {
ext = ".bin"
}
newName := uuid.NewString() + ext
避免用时间戳或哈希作主名
时间戳(如 time.Now().UnixNano())在高并发下易冲突;哈希(如 md5(fileBytes))需先读全文件,阻塞协程且浪费内存。UUID v4 是更轻量、分布安全的选择。
- UUID 不依赖文件内容,无需打开或读取文件本体
- 单次调用
uuid.NewString()平均耗时 - 若需更高唯一性保障,可用
uuid.Must(uuid.NewRandom())(但非必须) - 不要拼接用户 ID 或时间到文件名里——这会暴露业务信息,且无实际好处
保存前务必校验扩展名白名单
仅靠重命名不能替代类型检查。攻击者可上传 shell.php.jpg,服务端若只看扩展名就放行,配合解析漏洞可能触发执行。
- 用
mimeType := fh.Header.Get("Content-Type")做初筛(但可伪造) - 必须用
file.Header读取前 512 字节,调用http.DetectContentType()或github.com/h2non/filetype - 白名单应严格,例如只允许
["jpg", "jpeg", "png", "pdf"],拒绝".php", ".js", ".html" - 保存路径也需限定,如固定到
./uploads/,禁止写入../
Content-Type 或后缀。


















