如何安全校验ThinkPHP中上传图片的真实MIME类型?
- 内容介绍
- 文章标签
- 相关推荐
本文共计817个文字,预计阅读时间需要4分钟。
plaintext使用浏览器提交文件时,前端可以伪造文件类型。例如,将文本文件伪装成图片文件。具体做法如下:
- 务必关闭
FILEINFO_MIME_TYPE的FILEINFO_NO_EXTENSION选项,否则可能误判带 BOM 的 PNG - Windows 下需确认 php.ini 启用了
extension=fileinfo,否则finfo_open()直接报错 - ThinkPHP 6+ 的
validate(['file' => 'image'])默认只检查后缀和$_FILES['type'],不触发真实 MIME 校验
ThinkPHP 中手动校验上传文件 MIME 的正确写法
不能依赖框架默认验证规则,必须在控制器或中间件里用 finfo_file 重检。关键点是:必须用临时文件路径($_FILES['file']['tmp_name']),不能用移动后的路径,否则可能因权限或 SELinux 导致失败。
- 示例代码:
$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'])) { throw new \Exception('非法图片类型'); }
- ThinkPHP 6 的
UploadFile对象可通过$file->getRealPath()获取临时路径,等价于$_FILES['x']['tmp_name'] - 避免用
getimagesize()替代 —— 它对非标准头(如 CMYK JPEG)易失败,且无法识别 WebP、AVIF 等新格式
常见绕过场景与防御补丁
攻击者会构造「双扩展名」文件(shell.php.jpg)、嵌入恶意 PHP 代码的图片(图片末尾追加 <?php system($_GET[1]);?>)、或利用 GD 库解析漏洞(如 CVE-2019-17158)。仅校验 MIME 不够。
- 必须配合「重编码」:用
imagecreatefromxxx()+imageXXX()生成新文件,剥离所有元数据和隐藏代码 - 禁用危险函数:确保
php.ini中disable_functions包含exec,passthru,shell_exec,system - 上传目录禁止执行:Web 服务器配置中,对
uploads/目录显式禁用php_flag engine off(Apache)或location ~ \.php$ { deny all; }(Nginx)
ThinkPHP 6+ 自定义验证规则封装建议
把 MIME 校验逻辑抽成独立验证器,避免每个控制器重复写 finfo。注意:验证器类里不能直接访问 $_FILES,要通过 $value->getRealPath() 获取临时路径。
立即学习“PHP免费学习笔记(深入)”;
- 创建
app/validate/MimeImage.php,rule方法内调用finfo_file并返回布尔值 - 在控制器中:
$this->validate($data, 'app\validate\MimeImage.image') - 别在验证规则里调用
moveTo()—— ThinkPHP 会在验证通过后自动移动,重复操作会导致tmp_name失效 - 如果项目用 Swoole 或协程,注意
finfo资源不是线程安全的,每次校验都应finfo_open+finfo_close
真实环境里,MIME 校验只是第一道门,后面还有文件内容重绘、目录执行权限、HTTP 头限制三道坎。少走一步,就可能让一张「正常」的二维码变成 WebShell 入口。
本文共计817个文字,预计阅读时间需要4分钟。
plaintext使用浏览器提交文件时,前端可以伪造文件类型。例如,将文本文件伪装成图片文件。具体做法如下:
- 务必关闭
FILEINFO_MIME_TYPE的FILEINFO_NO_EXTENSION选项,否则可能误判带 BOM 的 PNG - Windows 下需确认 php.ini 启用了
extension=fileinfo,否则finfo_open()直接报错 - ThinkPHP 6+ 的
validate(['file' => 'image'])默认只检查后缀和$_FILES['type'],不触发真实 MIME 校验
ThinkPHP 中手动校验上传文件 MIME 的正确写法
不能依赖框架默认验证规则,必须在控制器或中间件里用 finfo_file 重检。关键点是:必须用临时文件路径($_FILES['file']['tmp_name']),不能用移动后的路径,否则可能因权限或 SELinux 导致失败。
- 示例代码:
$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'])) { throw new \Exception('非法图片类型'); }
- ThinkPHP 6 的
UploadFile对象可通过$file->getRealPath()获取临时路径,等价于$_FILES['x']['tmp_name'] - 避免用
getimagesize()替代 —— 它对非标准头(如 CMYK JPEG)易失败,且无法识别 WebP、AVIF 等新格式
常见绕过场景与防御补丁
攻击者会构造「双扩展名」文件(shell.php.jpg)、嵌入恶意 PHP 代码的图片(图片末尾追加 <?php system($_GET[1]);?>)、或利用 GD 库解析漏洞(如 CVE-2019-17158)。仅校验 MIME 不够。
- 必须配合「重编码」:用
imagecreatefromxxx()+imageXXX()生成新文件,剥离所有元数据和隐藏代码 - 禁用危险函数:确保
php.ini中disable_functions包含exec,passthru,shell_exec,system - 上传目录禁止执行:Web 服务器配置中,对
uploads/目录显式禁用php_flag engine off(Apache)或location ~ \.php$ { deny all; }(Nginx)
ThinkPHP 6+ 自定义验证规则封装建议
把 MIME 校验逻辑抽成独立验证器,避免每个控制器重复写 finfo。注意:验证器类里不能直接访问 $_FILES,要通过 $value->getRealPath() 获取临时路径。
立即学习“PHP免费学习笔记(深入)”;
- 创建
app/validate/MimeImage.php,rule方法内调用finfo_file并返回布尔值 - 在控制器中:
$this->validate($data, 'app\validate\MimeImage.image') - 别在验证规则里调用
moveTo()—— ThinkPHP 会在验证通过后自动移动,重复操作会导致tmp_name失效 - 如果项目用 Swoole 或协程,注意
finfo资源不是线程安全的,每次校验都应finfo_open+finfo_close
真实环境里,MIME 校验只是第一道门,后面还有文件内容重绘、目录执行权限、HTTP 头限制三道坎。少走一步,就可能让一张「正常」的二维码变成 WebShell 入口。

