必须基于文件二进制内容用魔术字节校验真实 MIME 类型,因 $_FILES'x' 和扩展名均可伪造;PHP 应用 finfo_file() 读取 tmp_name 并配合重绘图片、禁用上传目录脚本执行等二次防护。

不能只看浏览器传来的 Content-Type 或文件后缀,必须基于文件二进制内容做真实校验。核心是用系统级“魔术字节”识别,而非信任客户端输入。
为什么 $_FILES['x']['type'] 和文件扩展名不可靠
浏览器提交的 $_FILES['x']['type'] 完全由前端控制,可随意伪造——比如把恶意 PHP 文件的类型设为 image/jpeg,服务器若仅据此放行,就等于开门揖盗。同理,仅检查 .jpg 后缀毫无意义,攻击者上传 shell.php.jpg 就能绕过。
- 真实 MIME 类型取决于文件开头若干字节(magic bytes),与名称、头部字段无关
- PHP 的
finfo_file()调用系统 libmagic 数据库,直接读取临时文件二进制流,结果可信 - 务必确认 php.ini 中已启用
extension=fileinfo,否则finfo_open()会报错
PHP 中正确验证上传文件 MIME 的写法
关键点:必须使用 $_FILES['file']['tmp_name'](或 ThinkPHP/Laravel 中的 $file->getRealPath()),不能对已移动的文件路径调用 finfo_file(),否则可能因权限、SELinux 或 NFS 挂载问题失败。
- 示例代码:
<?php
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $_FILES['avatar']['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'])) {
throw new Exception('不支持的图片格式');
}
?>
- ThinkPHP 6+ 的 UploadFile 对象可用
$file->getRealPath()替代$_FILES数组 - 避免用
getimagesize()替代:它对 CMYK JPEG、无 EXIF 的 WebP 等易失败,且不支持 AVIF
光验 MIME 还不够:必须配合二次防护
MIME 正确只说明“它看起来像图”,不代表它安全。攻击者可在 PNG 末尾追加 PHP 代码,或构造含恶意元数据的图片,等待被 GD/Imagick 库解析时触发漏洞。
- 推荐做法:用
imagecreatefromxxx()+imageXXX()重绘并保存新文件,彻底剥离原始头信息和隐藏内容 - 上传目录必须禁用脚本执行:Nginx 配置
location ~ \.php$ { deny all; },Apache 用php_flag engine off - PHP 配置中应禁用危险函数:
disable_functions = exec,passthru,shell_exec,system
其他语言中的等效实践
Java 可用 javax.activation.MimetypesFileTypeMap,但要注意不同系统识别结果可能不一致,建议搭配文件头字节比对;Go 推荐使用 mimetype 库,它支持 200+ 格式并能逐层识别(如 ZIP → .docx);Laravel 内置 mimetypes: 规则即基于 finfo,可直接使用。

















