必须用finfo_file()读取文件魔数校验真实MIME类型,因后缀和$_FILES'file'均可被客户端伪造;校验须在moveTo前完成,配合白名单精确匹配、强制重命名及上传目录隔离。

PHP文件上传类型出错,不是代码写错了,而是校验方式用错了。光靠扩展名或浏览器传来的 $_FILES['file']['type'],等于把门锁换成贴纸——看着像,一碰就掉。
别信后缀,也别信Content-Type
用户把 shell.php 改成 shell.jpg,pathinfo($name, PATHINFO_EXTENSION) 就返回 jpg;用 curl 伪造 Content-Type: image/jpeg,$_FILES['file']['type'] 就真变成 image/jpeg。这两者都是客户端输入,和文件真实内容完全无关。
- Apache 可能按
.php.jpg最右的.php执行代码 - Nginx 虽不解析双后缀,但攻击者可换用
.php;.jpg(IIS 风格)绕过 - ThinkPHP 的
validate(['fileext' => 'jpg,png'])或 Yii2 的mimeTypes规则,若未触发真实探测,底层仍读的是这个伪造值
必须用 finfo_file() 读文件头
真正靠谱的方式,是打开临时文件,读前几个字节(魔数),让 PHP 调用 libmagic 数据库比对。这就是 finfo_file() 干的事,也是 $file->getMime()(ThinkPHP 6+)和 $model->imageFile->tempName(Yii2)背后的真实逻辑。
- 先确认
fileinfo扩展已启用:php -m | grep fileinfo,没输出就得在 php.ini 加extension=fileinfo并重启 PHP-FPM - 必须作用于
$_FILES['file']['tmp_name']或$file->getRealPath(),不能等文件 move 后再查 - 返回值如
image/jpeg、application/pdf,必须用in_array($realType, ['image/jpeg', 'application/pdf'], true)严格匹配,禁用strpos($realType, 'image/') === 0
校验通过后,还要重命名+绑定后缀
哪怕 finfo_file() 确认是 image/jpeg,也不能保留用户原名 report.php.jpg。得查映射表,强制赋予合法后缀:
- 映射表要一一对应:
['image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf'] - 生成新文件名推荐:
uniqid('up_') . bin2hex(random_bytes(8)) . '.' . $ext - 绝不能接受非标准 MIME 如
image/x-png,应直接拒绝,不映射、不降级
常见报空或 octet-stream?先查上传链路
如果 $file->getMime() 返回空或 application/octet-stream,大概率不是代码问题,而是上传过程被干扰:
- Nginx:检查
client_max_body_size是否太小,导致文件截断 - Apache + mod_security:可能重写了临时文件内容,破坏魔数
- Windows + IIS:临时路径含中文或空格时,
finfo_file()会静默失败,建议先move_uploaded_file()到安全路径再探测 - CLI 测试:
$_FILES为空,ThinkPHP 上传类不支持 CLI 模拟上传
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











