必须放弃仅靠后缀校验的思路,需结合白名单、小写比对、危险字符串过滤、真实mime校验(finfo_file)及安全重命名与存储,多层防御缺一不可。

ThinkPHP 文件上传仅靠后缀校验根本拦不住恶意文件,必须放弃“信任文件名”的思路——攻击者传 shell.php.jpg、webshell.PHP、1.php%00.png 都能轻松绕过。真正有效的后缀拦截,是把它作为多层防御中的一环,且必须配合真实内容校验和安全存储。
显式白名单 + 统一小写 + 拦截所有变体后缀
ThinkPHP 的 ext 规则(如 ['ext' => 'jpg,png,gif'])只检查文件名后缀,不读取内容。但它仍是必要基础,关键在于怎么用:
- 白名单必须手动列出全部允许项,不能留空、不能用通配符,例如:
['jpg', 'jpeg', 'png', 'gif', 'pdf', 'docx'] - 扩展名提取后强制转小写比对:
strtolower(pathinfo($name, PATHINFO_EXTENSION)),否则.PHP、.PHTML可能被漏掉 - 额外拦截危险后缀:在验证逻辑外加一层过滤,拒绝包含
php、phtml、phar、htaccess、user.ini等字符串的原始文件名(哪怕它带 .jpg)
后缀校验必须在 moveTo() 前完成,且不能单独依赖
ThinkPHP 的 validate 方法(TP6)或 FileValidate(TP5.1)只处理 $_FILES 数组字段,它无法识别双后缀或空字节截断。常见错误是:先调 $file->move() 再去校验,此时临时文件已移走,后续 finfo_file 会失败。
- TP5.1:用
FileValidate,规则键名必须是size和extension(对应 $_FILES 的原生字段) - TP6:需构造数组传入
Validate,含tmp_name、size、extension字段,再启用fileExt和fileMime规则 - 无论 TP 版本,都不要把
ext当唯一防线——它只是第一道筛子,不是闸门
必须搭配真实 MIME 校验,否则后缀检查形同虚设
后缀可伪造,但文件头(Magic Bytes)不能。finfo_file 是目前最轻量、最可靠的校验方式,必须在 moveTo() 前调用:
- 开启
fileinfo扩展(PHP 默认启用,Docker 镜像常禁用,需确认) - 用
finfo_open(FILEINFO_MIME_TYPE)获取真实类型,只接受明确值:image/jpeg、application/pdf,拒绝text/plain、application/octet-stream、application/x-php - 别用
getimagesize():对非图文件报错,且 JPEG APPx 段可绕过;也别只判断是否含image/,防止image/php类混淆
上传后立即重命名,彻底丢弃原始文件名
即使后缀和 MIME 都合法,攻击者仍可能利用 Web 服务器解析漏洞(如 Nginx fastcgi_split_path_info 截断、Apache 多后缀解析)执行恶意代码。解决办法是物理隔离:
- 生成新文件名:
uniqid() . '_' . mt_rand(100, 999) . '.' . $safeExt,不保留任何用户输入字符 - 目标路径绝不能是
public/uploads/这类 Web 可直访目录,应选runtime/upload/或storage/app/uploads/ - 若业务必须对外提供访问,走 PHP 下载接口(readfile + header),不暴露真实路径
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











