必须用 finfo_file 校验真实 mime 类型,禁用后缀信任,上传目录隔离并禁用脚本执行,move 前重命名文件。

上传后立刻用 finfo_file 检查真实 MIME 类型
用户传的 .jpg 文件可能只是外壳,实际是 PHP 木马。浏览器或客户端改后缀、伪造 Content-Type 很容易,不能信 $_FILES['file']['type'] 或文件扩展名。
ThinkPHP 默认的 validate(['ext' => 'jpg,png,gif']) 只校验后缀,毫无防御力。必须读取文件头做二进制识别:
-
finfo_open(FILEINFO_MIME_TYPE)是最轻量、最可靠的方式,PHP 内置,无需额外扩展 - 务必在
move_uploaded_file()之前调用,否则临时文件可能被删或权限受限 - 常见错误:直接用
getimagesize()—— 它对非图像文件会警告甚至失败,且可被构造的 JPEG APPx 段绕过
use think\File;
$file = request()->file('image');
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$realType = finfo_file($finfo, $file->getTmpName());
finfo_close($finfo);
if (!in_array($realType, ['image/jpeg', 'image/png', 'image/gif'])) {
throw new \Exception('文件类型不合法:' . $realType);
}
ThinkPHP 的 validate 配置要禁用后缀信任模式
ThinkPHP 6.x 的 validate 方法默认只看扩展名,['ext' => 'jpg,png'] 这种写法等于摆设。必须配合 mime 规则 + 自定义验证器,否则白忙活。
-
mime规则本身仍依赖$_FILES['file']['type'],不可单独使用 - 正确做法:把
finfo_file校验逻辑封装成自定义验证规则,例如realImage - 别在控制器里手写校验逻辑再调
$file->move()—— ThinkPHP 的validate支持闭包,可直接嵌入真实类型检查
$file->validate([
'size' => 2097152,
'ext' => 'jpg,png,gif',
'callback' => function ($file) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $file->getTmpName());
finfo_close($finfo);
return in_array($type, ['image/jpeg', 'image/png', 'image/gif']);
}
]);
上传目录必须与 Web 访问路径隔离 + 禁用脚本执行
就算文件类型校验通过,攻击者仍可能利用 Nginx/Apache 解析漏洞(如 .jpg/.php 双后缀)、或上传带 PHP 标签的图片(exif 注释区注入),让服务器误执行。
- 绝对不要把上传文件放在
public/或任何 Web 可直访目录下;ThinkPHP 应存到runtime/upload/或独立域名静态资源站 - Nginx 需显式禁止上传目录下的脚本执行:
location ^~ /upload/ { deny all; }或更细粒度地location ~ /upload/.*\.(php|php5|phtml)$ { return 403; } - Apache 要确认
.htaccess在上传目录生效,且含php_flag engine off或RemoveHandler .php
二次校验不是“多加一层保险”,而是唯一可信环节
前端校验、后缀过滤、Content-Type 检查全不可信。有人觉得“我用了七层验证”,其实前六层全是幻觉,只有 finfo_file + 目录隔离 + Web 服务配置这三项落地了,才算真正守住入口。
最容易被跳过的其实是最后一步:上传成功后,没重命名文件就直接返回给前端 URL,结果攻击者用原文件名(含双后缀)发起请求,绕过所有校验。ThinkPHP 的 $file->move() 必须强制指定新文件名,且不含用户可控字段。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










