必须用 finfo_file() 校验真实 MIME 类型,因 $_FILES'file' 和 ThinkPHP 的 validate(['mime'=>...]) 均依赖可伪造的客户端类型;应基于临时文件二进制头部,用 libmagic 比对魔数,并严格白名单匹配。

必须用 finfo_file() 基于二进制头部校验真实 MIME 类型,不能依赖 $_FILES['file']['type'] 或 ThinkPHP 的 validate(['mime' => ...]),因为它们都可被客户端随意伪造。
为什么浏览器传的 type 和框架 validate 都不可信
用户上传时,浏览器会根据文件扩展名或本地注册表设置 Content-Type,这个值完全由前端控制,比如把 shell.php 改名为 shell.jpg 并声明为 image/jpeg,服务端收到的就是假类型。ThinkPHP 6.x 的 validate(['mime' => 'image/png']) 默认仍读取这个伪造字段,除非你显式调用 $file->getMime() 触发底层探测。
正确使用 finfo_file() 校验真实类型
可靠方式是读取临时文件($file->getRealPath() 或 $_FILES['x']['tmp_name'])的二进制头,交由系统 libmagic 数据库比对魔数(magic bytes):
- 确保 PHP 已启用
fileinfo扩展:运行php -m | grep fileinfo,无输出需在php.ini中开启extension=fileinfo - 在
$file->move()之前调用:$realType = $file->getMime();(ThinkPHP 封装版)或手动用finfo_open(FILEINFO_MIME_TYPE) - 白名单必须严格匹配完整字符串:
in_array($realType, ['image/jpeg', 'image/png'], true),避免用strpos模糊判断 - 若返回
application/octet-stream或空值,可能是文件过小、损坏,或 magic db 不覆盖,此时应补充文件头硬校验(如前 8 字节是否为89504e47判 PNG)
绕过 finfo 的兜底方案:手动读取魔数
当环境不支持 fileinfo(如某些精简 Docker 镜像),或需增强确定性时,可直接读取文件前若干字节做比对:
立即学习“PHP免费学习笔记(深入)”;
- 用
fopen($path, 'rb')以二进制模式打开,防止 Windows 下换行符转换干扰 - 读取前 16 字节(足够覆盖常见格式魔数),转为十六进制后比对已知签名,例如:
ffd8ff→ JPEG,89504e47→ PNG,52617221→ RAR - 不要用
file_get_contents()读大文件,改用fread($fp, 16)+fclose()控制内存
常见陷阱与规避建议
校验失败不一定是代码写错,更可能是上传链路异常:
- Nginx 用户检查
client_max_body_size是否过小,导致文件被截断,finfo 无法识别 - Apache + mod_security 可能重写临时文件内容,破坏魔数
- Windows 下
tmp_name含中文或空格时,finfo_file()可能静默失败,建议先move_uploaded_file()到安全路径再探测 - 绝对不要用已废弃的
mime_content_type(),它在 PHP 7.2+ 被标记为 deprecated,行为不稳定且不支持自定义 magic 库



















