$_FILES'file'完全不可信,因其由浏览器伪造;必须用finfo_file()读取文件头校验真实MIME类型,并配合扩展名白名单、上传目录禁解析及Web目录外隔离等多重防御。

$_FILES['file']['type'] 完全不可信,别用它做校验
PHP中 $_FILES['file']['type'] 是浏览器提交的 Content-Type 字段值,服务端未做任何验证就直接读取——等于把文件类型判定权交给了攻击者。比如上传一个 shell.php,只要前端或抓包时把 Content-Type 改成 image/jpeg,$_FILES['file']['type'] 就会显示为 image/jpeg,而实际内容和后缀毫无变化。
常见错误写法:
if ($_FILES['file']['type'] !== 'image/png') {
die('仅允许 PNG 文件');
}
这种代码在 Burp 中改一行 Content-Type 就能绕过。真实项目里必须废弃该字段,改用服务端真实读取文件内容的方式判断。
finfo_file() 是 MIME 校验的底线,但需配合白名单
finfo_file() 通过读取文件头(魔数)识别真实 MIME 类型,比 $_FILES['file']['type'] 可靠得多。但它仍不是万能的:头部可被手动构造、某些格式兼容性差(如 WebP 在旧版 libmagic 中识别不准),且不校验扩展名。
立即学习“PHP免费学习笔记(深入)”;
实操要点:
- 必须用
FILEINFO_MIME_TYPE模式,避免返回带参数的完整 MIME(如image/jpeg; charset=binary) - 结果必须与严格白名单比对,不能用
strpos()或模糊匹配 - 要先检查
finfo_open()是否成功,否则可能返回空或错误类型 - 只校验 MIME 不够,必须同步校验扩展名是否在白名单内(如
.png对应image/png)
示例关键逻辑:
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
$allowed = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mimeType, $allowed)) {
die('MIME 类型不合法');
}
绕过 finfo_file() 的常见手法及应对
攻击者可通过拼接图片头 + PHP 代码(即“图片马”)绕过 finfo_file()。因为 finfo 只读前几百字节,只要开头是合法图片魔数(如 GIF89a),就会返回 image/gif,后面追加的 <?php phpinfo();?> 不影响识别。
此时单靠 MIME 校验已失效,必须叠加其他手段:
- 对图片类文件,调用
getimagesize()或exif_imagetype()进一步验证——它们会解析更深层结构,普通图片马常在此失败 - 启用 GD 库二次渲染:
imagecreatefrompng()+imagepng(),可剥离末尾注入代码 - 若非必须保留原始文件,直接拒绝所有含 PHP 标签(
<?php、<script)或可疑函数名(eval、system)的文件内容
上传目录禁解析才是最后一道防线
即使 MIME 和扩展名校验都绕过了,只要上传目录配置为禁止执行脚本,攻击者就无法直接触发代码。这是成本最低、效果最硬的兜底措施。
Apache 用户应在上传目录下放一个 .htaccess:
php_flag engine off RemoveHandler .php .phtml .php3 .php4 .php5 .php7 .php8
Nginx 用户则在对应 location 块中加入:
location ~ \.(php|phtml|php3|php4|php5|php7|php8)$ {
deny all;
}
注意:这个配置必须作用于实际存放上传文件的路径,而不是 Web 根目录的某个子目录——如果路径可被遍历或别名映射绕过,照样失效。
真正容易被忽略的是权限隔离:上传后的文件不应放在 Web 可访问路径下,理想做法是存到 /var/data/uploads/ 这类非 Web 目录,再通过代理脚本(如 /api/download?id=123)控制访问。一旦路径和解析双重失守,前面所有校验都形同虚设。



















