最可靠的方式是用 pathinfo() 提取扩展名后严格比对硬编码白名单:先 strtolower() 统一大小写,再 in_array() 校验;需过滤反斜杠、校验扩展名非空,并限制文件名长度以防截断。

用 pathinfo() 提取扩展名再比对白名单最可靠
直接读取文件名后缀字符串(比如用 substr(strrchr($filename, '.'), 1))容易被绕过,例如 shell.php.jpg 或 image.jpg.php。PHP 的 pathinfo() 能按标准路径规则解析,返回的 extension 是最后一段点号后的部分,但真正安全的做法是:只信任它提取出的扩展名,再严格比对预设白名单。
示例:
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
if (!in_array($ext, $allowed)) {
throw new InvalidArgumentException('不支持的文件扩展名');
}
- 务必用
strtolower()统一小写,避免JPG和jpg被当成不同扩展名 - 白名单必须硬编码或从可信配置加载,不能从用户输入、数据库动态拼接
-
PATHINFO_EXTENSION只取最后一段,所以test.tar.gz返回gz,不是tar.gz—— 这正是你想要的,避免误判归档包内嵌脚本
别信 mime_content_type() 或 finfo_file() 做唯一判断
MIME 类型检测(如 finfo_file(finfo_open(FILEINFO_MIME_TYPE), $tmpfile))能识别文件真实内容,但它解决的是「文件是不是图片」这类问题,和「扩展名是否合法」是两件事。上传场景中,你通常需要同时满足:扩展名在白名单内 + 内容类型匹配预期。只靠 MIME 检测会漏掉扩展名伪造(比如把恶意 PHP 文件改成 .jpg 后缀但内容仍是 PHP),而只靠扩展名又会误杀真实图片(比如某些相机生成的 .arw 文件)。
- 扩展名合法性检查必须独立做,且优先于 MIME 检查
-
finfo_file()需要临时文件路径,不能直接用于未保存的上传流;mime_content_type()已被标记为废弃 - 若需双重校验,顺序应为:先验扩展名 → 再存临时文件 → 再用
finfo_file()验内容 → 最后才移动到正式位置
$_FILES['file']['name'] 的扩展名可能被客户端篡改,必须重解析
浏览器上传时,$_FILES['file']['name'] 完全由前端控制,可任意构造。攻击者可能提交 webshell.php 却把表单字段 name 改成 avatar.jpg,或者在 filename 中插入空字节、Unicode 零宽字符干扰解析。PHP 不会自动清理这些。
立即学习“PHP免费学习笔记(深入)”;
- 永远不要直接使用
$_FILES['file']['name']拼接存储路径或判断类型 - 必须用
pathinfo()在服务端重新解析该字段值,且只取PATHINFO_EXTENSION - 如果业务允许,更稳妥的方式是忽略原始文件名,服务端生成唯一文件名(如
sha256($content).jpg),仅保留扩展名用于 Content-Type 响应
注意 Windows 路径中的反斜杠和长文件名截断陷阱
在 Windows 环境下,$_FILES['file']['name'] 可能含 \(虽然罕见),pathinfo() 对反斜杠的处理与正斜杠不一致,可能导致提取失败。另外,超长文件名(如带大量 Unicode 字符)在某些 PHP 版本中会被截断,使 pathinfo() 返回空扩展名。
- 上传前先用
str_replace('\\', '/', $_FILES['file']['name'])统一路径分隔符 - 检查
pathinfo($filename, PATHINFO_EXTENSION)是否为空字符串,空则拒绝 —— 这可能是截断或非法格式信号 - PHP 7.4+ 对长文件名支持更好,但生产环境仍建议限制
$_FILES['file']['name']总长度(如 ≤ 255 字节)











