php 8.2 文件校验须基于内容,采用 finfo 探测 mime、magic bytes 手动校验、扩展名与 mime 双向匹配三重防护,并上传后重命名隔离存储且禁用脚本执行。

PHP 8.2 文件类型校验不能只依赖客户端传来的 Content-Type 或文件扩展名——这两者极易伪造。真实有效的校验必须基于文件内容本身,结合 MIME 类型探测、二进制签名(magic bytes)和白名单策略三重防线。
用 finfo 探测真实 MIME 类型
PHP 内置的 finfo 扩展(基于 libmagic)是校验文件本质类型的首选工具。它读取文件开头若干字节,比对已知签名库,返回可信的 mime/type。
- 务必使用
FILEINFO_RAW模式配合finfo_buffer(),避免因临时文件路径问题绕过检测 - 禁用
finfo_file()直接传路径的方式——上传临时文件可能被并发请求篡改或替换 - 示例代码(可直接用于 Laravel Request 验证逻辑中):
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_buffer($finfo, file_get_contents($request->file('avatar')->getRealPath()));
finfo_close($finfo);
if (!in_array($mime, ['image/jpeg', 'image/png', 'image/webp'], true)) {
throw new ValidationException('不支持的图片格式');
}
手动校验 Magic Bytes 防绕过
某些边缘场景(如 libmagic 库未更新、自定义格式)下,finfo 可能误判。此时应补充原始字节比对,精准识别常见类型头部:
- JPEG:以
\xFF\xD8\xFF开头 - PNG:以
\x89PNG\r\n\x1A\n开头 - PDF:以
%PDF-开头(注意前导空格和版本号) - ZIP(含 DOCX/XLSX):以
\x50\x4B\x03\x04开头
推荐写法(安全、无依赖、可复用):
$raw = file_get_contents($uploadedFile->getRealPath(), false, null, 0, 8);
switch (true) {
case str_starts_with($raw, "\xFF\xD8\xFF"): $type = 'jpeg'; break;
case str_starts_with($raw, "\x89PNG\r\n\x1A\n"): $type = 'png'; break;
case str_starts_with($raw, '%PDF-'): $type = 'pdf'; break;
case str_starts_with($raw, "\x50\x4B\x03\x04"): $type = 'zip'; break;
default: throw new ValidationException('非法文件头');
}
扩展名与 MIME 必须双向匹配
仅校验 MIME 或仅校验扩展名都存在风险。攻击者可上传 shell.php.jpg(扩展名合法但内容恶意),或构造 photo.jpg 实际为 PHP 脚本(MIME 声明为 image/jpeg 但内容可执行)。
- 提取扩展名用
pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION),不信任$file->extension()(可能被伪造) - 建立严格映射表,例如:
['jpg','jpeg','jpe'] → 'image/jpeg',['png'] → 'image/png' - 校验失败时统一拒绝,不提示具体哪项出错(防信息泄露)
上传后立即重命名并隔离存储
校验通过不代表万事大吉。必须切断原始文件名与服务端路径的关联,防止目录遍历、解析绕过等二次攻击:
- 生成唯一文件名:
sha256_file($tmpPath) . '.' . $safeExtension - 存储路径不使用用户可控字段(如
/uploads/{user_id}/{filename}),而用哈希分片:/uploads/ab/cd/ef/... - Web 服务器(Nginx/Apache)需配置禁止执行上传目录下的脚本:
location ~ ^/uploads/.*\.(php|phar|phtml)$ { return 403; }
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











