必须用finfo_file()读取文件魔数校验真实MIME类型,因ext和mimeTypes规则仅依赖客户端可控的后缀与Content-Type字段,无法识别shell.php.jpg等伪装文件;校验须在moveTo前完成,且需配合隔离存储、禁用脚本执行及强制重命名。

因为仅靠后缀或 $_FILES['file']['type'] 校验完全无效——攻击者能轻易伪造,上传的 shell.php.jpg 或声明为 image/jpeg 的 PHP 文件,会直接绕过所有基于字符串匹配的规则。
为什么 ext 和 mimeTypes 规则都拦不住木马
ThinkPHP 的 ext 规则只取 $_FILES['file']['name'] 的后缀,mimeTypes 则比对 $_FILES['file']['type'] 字段,两者全由客户端控制。浏览器、抓包工具甚至 curl 都能任意修改它们。
- 用户上传
1.php.jpg,ext提取到jpg→ 通过 - 用户篡改请求头
Content-Type: image/jpeg,mimeTypes→ 通过 - 文件真实内容是 PHP 代码,但框架没读一个字节 → 执行环境里就多了一个 WebShell
必须用 finfo_file() 读魔数,不是“可选优化”
finfo_file() 基于文件前几百字节的 magic bytes 判断类型,不依赖任何客户端输入,是 PHP 官方推荐的唯一可靠方式。它调用系统 libmagic 数据库,识别精度远高于后缀或 header 字段。
- 必须传入临时文件路径:
$file->getRealPath()或$_FILES['x']['tmp_name'],不能传移动后的路径 - 务必确认
extension=fileinfo已在php.ini中启用,否则finfo_open()直接返回false - 不要用
getimagesize()替代:它对非标准图像(CMYK JPEG、WebP、AVIF)支持弱,且可能被构造的 APPx 段绕过
验证时机错一步,整个校验就失效
真实 MIME 校验必须在 $file->moveTo() 之前完成。一旦文件落地,临时文件可能被清理、权限变化,或 SELinux 策略阻止再次读取。
立即学习“PHP免费学习笔记(深入)”;
- ThinkPHP 6+ 的
$file->getMime()是封装好的finfo_file()调用,可用,但前提是fileinfo已启用 - 别把校验写在 move 之后——那时木马已存盘,再删也晚了
- 别在 validate 规则里混用
ext和type:TP 默认优先匹配ext,type根本不会触发
校验通过 ≠ 安全,后续动作一个都不能少
即使 finfo_file() 返回 image/jpeg,也不能放松——图片末尾可能追加 PHP 代码,exif 区域可能藏恶意 payload,Nginx/Apache 还可能按双后缀解析执行。
- 上传目录必须隔离在
webroot外(如runtime/upload/),绝不能放在public/下 - Web 服务器需显式禁用脚本执行:
location ~ \.php$ { deny all; }(Nginx)或php_flag engine off(Apache) - 强制重命名文件,丢弃客户端原始名;最好用
exif_imagetype()再做一次轻量结构校验,配合扩展名映射表生成新后缀
最常被忽略的点:开发者以为配了 validate(['ext' => 'jpg,png']) 就万事大吉,却没意识到这行代码根本没碰文件内容——它只在字符串层面做切片和比对,而攻击者从不上传“真图片”。



















