必须将 pathinfo($filename, pathinfo_extension) 返回的扩展名转小写,因为其严格保留原始大小写,而白名单通常为小写,不转换会导致 in_array 匹配失败。

直接用 pathinfo($filename, PATHINFO_EXTENSION) 不加 strtolower(),就会漏判 JPG、PNG 这类大写扩展名——它返回的是原始大小写,而白名单通常全小写。
为什么 pathinfo 返回的扩展名必须转小写
pathinfo() 严格保留原始字符串大小写,不自动标准化。浏览器、操作系统、用户上传工具都可能生成 IMG.JPG、report.PDF 或混合大小写如 Script.Php。如果你的白名单是 ['jpg', 'png', 'pdf'],但没调 strtolower(),那 in_array($ext, $allowed) 就会直接失败。
- 不转换:$ext = 'JPG' →
in_array('JPG', ['jpg'])返回false - 转换后:$ext = strtolower('JPG') → 'jpg' → 匹配成功
- 注意:
mb_strtolower()非必需,ASCII 扩展名用strtolower()完全够用,且更快
常见错误写法:只取 extension 却忽略空值和路径干扰
直接写 $ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION) 是危险的起点——它可能返回空字符串,或被恶意构造的文件名绕过。
- 文件名无点号(如
README)→$ext为空字符串,in_array('', $allowed)可能意外通过(若白名单里误含空值) - 文件名以点开头(如
.htaccess)→$ext是htaccess,不是你想拦的隐藏文件 - 路径含斜杠(如
../shell.php)→pathinfo()仍只取最后一段,但你要先过滤掉路径遍历字符 - 建议前置校验:
if (empty($ext) || preg_match('/[\\/ ]/', $_FILES['file']['name'])) { throw new InvalidArgumentException('非法文件名'); }
别用 explode + end 做扩展名提取
这种写法在真实上传场景中极易被绕过,比如 shell.php.jpg 会被判成 jpg,看似安全,实则放行了带 PHP 内容的文件。
-
$ext = end(explode('.', $_FILES['file']['name']))→ 对evil.PHP返回PHP,大小写不统一 -
$ext = substr(strrchr($_FILES['file']['name'], '.'), 1)→ 对.gitignore返回gitignore(错误),对noext触发警告 -
pathinfo()是唯一能按标准路径规则解析、天然隔离多点干扰的方式,且性能开销可忽略
白名单必须硬编码,且不能含通配符
扩展名校验不是模糊匹配,是精确准入控制。任何试图用正则、strpos() 或通配数组(如 ['image/*'])的做法都会失效。
- 错误:
if (strpos($ext, 'jpg') !== false)→jpeg被放过,phpjpg也被误判 - 错误:
$allowed = ['image']; in_array(substr($ext, 0, 5), $allowed)→ 完全不可控 - 正确:
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'txt'];,且仅用于in_array(strtolower($ext), $allowed) - 特别注意:
tar.gz类归档文件,pathinfo()返回gz,你得决定是否允许gz,而不是试图还原双扩展名——绝大多数业务不需要
最易被忽略的一点:pathinfo() 解析的是 $_FILES['file']['name'] 字符串,但它本身不校验该字符串是否合法。你必须在调用前清理空字节、Unicode 零宽字符,并确认它不包含路径分隔符;否则,哪怕扩展名对了,攻击者也能把文件写到任意目录。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











