必须读取文件前若干字节(魔术字节)比对签名才能精准识别类型,因$_files'file'和扩展名均可被伪造;finfo_file()依赖libmagic、结果稳定,而mime_content_type()已废弃且行为不一致。

仅靠 $_FILES['file']['type'] 或文件扩展名识别上传文件类型,根本不可信——它既可被浏览器伪造,也能被攻击者手动篡改。真正能落地的精准识别,必须读取文件前若干字节(即魔术字节 / Magic Number),并比对已知签名。
为什么 finfo_file() 是首选,而不是 mime_content_type()
mime_content_type() 已被 PHP 官方标记为废弃(E_DEPRECATED),且在不同系统上行为不一致:有的返回 image/jpeg,有的带 ; charset=binary 后缀,解析容易出错。而 finfo_file() 依赖 libmagic 数据库,基于真实文件头分析,结果稳定、语义清晰。
- 必须启用
fileinfo扩展(用php -m | grep fileinfo验证) - 创建
finfo实例时,务必传入FILEINFO_MIME_TYPE,避免返回完整 MIME 字符串干扰判断 - 不要对已移动的文件做检测——
$_FILES['file']['tmp_name']是唯一可信的临时路径,验证必须在move_uploaded_file()之前完成 - 调用前先确认
is_uploaded_file($_FILES['file']['tmp_name'])返回true,防止路径穿越(如传入../../etc/passwd)
如何手写魔术字节校验(绕过 finfo 限制场景)
某些受限环境(如共享主机禁用 fileinfo)需手动读取前 8 字节转十六进制,与标准签名比对。注意:不是所有格式都靠前 2 字节,PNG、PDF、ZIP 等需更多字节才能准确区分。
- 用
fopen($path, 'rb')以二进制方式打开临时文件 - 用
fread($fp, 8)读取最多 8 字节(覆盖 JPG、PNG、GIF、PDF、ZIP 常见头) - 用
bin2hex()转为小写十六进制字符串,再匹配(例如ffd8ff对应 JPG,89504e47对应 PNG) - 特别注意 GIF:前 6 字节是
474946383761或474946383961(即 GIF87a / GIF89a),不能只截前 4 字节 - PDF 必须检查前 4 字节是否为
25504446(%PDF),且第 5 字节常为2d(ASCII '-'),否则可能是伪造
常见误判点和白名单设计陷阱
即使拿到正确 MIME 类型,直接用 strpos($mimeType, 'image/') 判断图片仍危险:SVG(image/svg+xml)可嵌入 JS,WebP(image/webp)在老版本 PHP 中可能未被 finfo 支持,而 image/jpg 根本不是标准类型——实际只有 image/jpeg。
- 白名单必须显式列出允许值:
['image/jpeg', 'image/png', 'image/gif', 'application/pdf'] - 视频类型如
video/mp4需额外注意:部分 MP4 文件头与 ZIP 相同(504b0304),单靠魔数易混淆,此时finfo_file()的综合判断更可靠 - 对图像类文件,可叠加
exif_imagetype()做二次验证(仅支持图像,返回整型常量如IMAGETYPE_JPEG) - 不要忽略空文件或超小文件(如 0 字节):
finfo_file()可能返回text/plain或失败,需提前filesize()检查
魔数校验本身不难,难的是把「读哪几字节」「怎么转」「跟谁比」「比完之后怎么联动白名单和业务逻辑」这整条链路闭环。尤其当你要支持 WebP、AVIF、HEIC 等新格式时,libmagic 数据库版本、PHP 版本、finfo 初始化方式,任何一个环节掉链子都会让魔数失效。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











