应使用 finfo_open() + finfo_buffer() 校验文件真实 mime 类型,结合映射表获取可信后缀,再用随机文件名保存;禁用 $_files'file' 和 path.ext(),避免空字节与路径遍历风险。

用 finfo_open() 读文件头,别信 $_FILES['file']['type']
浏览器传来的 $_FILES['file']['type'] 是客户端自己填的,改个请求头就能变成 application/x-php,完全不可信。Echo 框架本身不内置 MIME 校验,得你自己在 FormFile 之后立刻用 PHP 的 finfo_open() 查真实类型。
关键点:必须基于临时文件路径(file.Open() 返回的 src 是 io.ReadSeeker,不能直接喂给 finfo_file();得先写到磁盘或用 finfo_buffer())。
推荐做法是:调用 file.Open() 后,用 io.Copy 把前几百字节读进内存 buffer,再用 finfo_buffer() 判断——既避开写磁盘开销,又绕过流不可 rewind 的问题。
in_array() 白名单校验必须配合真实后缀提取
只用 path.Ext(file.Filename) 提取后缀是危险的——攻击者传 shell.php.jpg,path.Ext() 返回 .jpg,但实际可能是 PHP 文件。正确做法是:先用 finfo_buffer() 得到真实 MIME,再映射成可信后缀(比如 image/jpeg → .jpg),最后用 in_array() 对照白名单数组比对。
常见错误:
- 把白名单写成
["jpg", "jpeg", "png"],但finfo_buffer()返回的是image/jpg(不是标准 MIME),导致匹配失败 - 没做大小写归一化,
image/JPEG和image/jpeg被当成两个类型 - 用
strings.HasSuffix(file.Filename, ".pdf")替代 MIME 校验,等于开门揖盗
为什么不用 mime_content_type()?它在 Echo 场景下容易失效
mime_content_type() 看起来更短,但它依赖临时文件路径,而 Echo 的 c.FormFile("xxx") 返回的 *multipart.FileHeader 只提供 Filename 和 Size,不保证 tmp_name 可用(尤其在自定义 MaxMemory 或启用了 SaveMultipartForm 时)。一旦 mime_content_type("/tmp/phpXXXXXX") 返回 false,你连 fallback 都没机会做。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
相比之下,finfo_open(FILEINFO_MIME_TYPE) + finfo_buffer($buf) 完全不依赖磁盘路径,只要读到前 256 字节就足够识别 PDF、PNG、JPEG、ZIP 等绝大多数格式。
示例关键片段:
file, _ := c.FormFile("document")
src, _ := file.Open()
defer src.Close()
buf := make([]byte, 256)
src.Read(buf) // 忽略 error,finfo_buffer 能处理不完整 buffer
f := finfo_open(FILEINFO_MIME_TYPE)
mimeType := finfo_buffer(f, buf)
finfo_close(f)
extMap := map[string]string{
"application/pdf": ".pdf",
"image/jpeg": ".jpg",
"image/png": ".png",
}
realExt, ok := extMap[strings.ToLower(mimeType)]
if !ok {
return c.JSON(http.StatusBadRequest, "invalid file type")
}
上传路径里别拼接原始 Filename,随机名 + 真实后缀才是安全组合
即使通过了 MIME 和白名单校验,也不能直接用 file.Filename 构造保存路径——攻击者可能在文件名里塞 ../../.htaccess 或空字节截断。Echo 不自动清理这些。
正确姿势是:
- 生成随机前缀(如
uuid.New().String()) - 拼上从 MIME 映射出的真实后缀(不是用户传的)
- 用
os.Create(filepath.Join(uploadDir, safeName))写入
注意:不要用 path.Base(file.Filename) 做二次过滤——它对 foo%00bar.jpg 这类空字节无效,且不解决路径遍历问题。真实后缀必须来自 finfo_buffer() 结果,不是字符串拆分。
最易被忽略的一点:校验和重命名必须在同一个逻辑分支里完成。如果先校验、再另起一个函数保存,中间可能被并发请求篡改临时文件内容——虽然概率低,但在金融或政务系统里,这属于必须堵死的缝隙。










