validate()仅校验后缀,无法识别shell.jpg等伪装文件;必须结合getmime()真实mime探测、魔数校验、白名单全等匹配、安全重命名及非web根目录存储。

validate() 只校验后缀,不能防 shell.jpg.php
ThinkPHP6 的 $file->validate(['ext' => 'jpg,png']) 仅提取客户端传来的原始文件名后缀(如 shell.jpg.php 提取的是 php),但若用户上传的是 shell.jpg(实际内容为 PHP 代码),该方法完全无法识别。它不读取文件内容、不调用 finfo_file()、也不校验 MIME,纯属字符串匹配。常见错误是以为加了 ext 就安全了,结果攻击者上传带 PHP 标签的图片,配合 Nginx 解析漏洞直接 RCE。
-
ext规则必须全小写、无点、逗号分隔,写成'JPG,PNG'或'.jpg,.png'都会失效 - size 单位是字节,
2 * 1024 * 1024这种表达式不能直接写进数组,得算成2097152或用常量 - validate() 返回
false时,$file->getError()才能拿到具体原因,不检查返回值就调move()会触发 “Uploaded file is not valid” 错误
getMime() 必须在 move() 前调用,且要处理 application/octet-stream
$file->getMime() 是 ThinkPHP6 封装的 finfo_file(),但它只在 fileinfo 扩展启用时有效,且必须在 move() 之前执行——因为 move() 会删除临时文件,$file->getRealPath() 指向的路径将失效,finfo_file() 会返回 false 或 application/octet-stream。后者不是“非法”,可能是文件太小、magic db 未覆盖,或 Nginx client_max_body_size 截断导致头部损坏。
- 先运行
php -m | grep fileinfo确认扩展已启用,否则getMime()恒返回空 - 若
$realType === 'application/octet-stream'或为空,必须 fallback 到魔数校验:用fopen($file->getRealPath(), 'rb')读前 8 字节,转 hex 后比对ffd8ff(JPEG)、89504e47(PNG) - 禁止用
strpos($realType, 'image/') === 0模糊匹配,application/x-php会被放行
白名单必须全等匹配 + 强制重命名 + 目录隔离
即使 getMime() 返回 image/jpeg,也不能直接信任——攻击者可构造含 PHP 标签的 JPEG(如 EXIF 注释区注入),或利用 Apache AddHandler 配置让 .jpg 当脚本执行。所以真实 MIME 校验只是第一道防线,后续动作缺一不可:
- 白名单用严格全等:
in_array($realType, ['image/jpeg', 'image/png', 'image/gif'], true) - 重命名必须丢弃原始名,用
uniqid() . '.' . $safeExt生成,且$safeExt要从白名单里取,不是从客户端推导 - 上传路径不能是
public/uploads/,必须是runtime/upload/或独立域名;若必须放 public 下,Nginx 需加location ~ \.(php|phtml|phar)$ { deny all; } - ThinkPHP6 推荐用
Filesystem::disk('public')->putFile()替代$file->move(),它自动重命名、支持远程存储、路径跨平台,但注意它仍不自动做 MIME 探测,validate()得前置调用
多文件上传每个 File 实例都要单独 validate()
TP6.0.13+ 支持 $request->file('images') 返回数组,但数组里每个元素仍是独立的 think\File 实例。它们的校验状态互不影响,且 validate() 是一次性操作:校验失败后不能再调 move(),校验成功后也不能复用同一实例多次 move()(第二次会因源文件不存在而报错)。批量场景下最容易踩的坑是“统一校验一次,然后循环 move”,这会导致部分文件跳过校验。
- 必须对每个
$file单独调$file->validate($rule),并检查返回值 - 验证失败时不要立即
continue,建议先move()到临时目录(如runtime/upload_tmp/),再统一清理或记录日志 - 并发上传时,目标子目录要用唯一标识(如
date('Ymd').'/'.uniqid()),避免写冲突 - 别依赖
$_FILES['images']['name'][0]这类原始数组结构,TP6 不原生解析多维$_FILES,前端必须用name="images[]"标准格式
真实 MIME 探测和魔数校验不是“可选项”,是绕不过去的硬性步骤;重命名和目录隔离也不是“部署习惯”,而是 Web 服务器配置失守时的最后一道缓冲。漏掉任意一环,shell.jpg 就可能变成 shell.php。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











