只校验后缀名根本挡不住木马,因攻击者可上传shell.php.jpg绕过;必须用finfo_file()校验真实MIME类型、显式白名单后缀、统一小写比对、重命名并禁用原始名中点和斜杠、上传目录禁用PHP执行。

只校验后缀名根本挡不住木马
用户传个 shell.php.jpg,你用 pathinfo($file['name'], PATHINFO_EXTENSION) 拿到 jpg 就放行,木马已经落地了。Apache、Nginx 在某些配置下会从右往左解析后缀,.php.jpg 仍可能被当作 PHP 执行。
ThinkPHP 的 validate(['ext' => 'jpg,png']) 默认只看后缀,不读文件头——这和安全无关,只是格式过滤。
- 白名单必须显式列出所有允许后缀:
['jpg', 'jpeg', 'png', 'gif'],别写通配符或留空 - 统一转小写比对:
strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)) - 额外拦截危险扩展:
php、phtml、php5、phar、.htaccess(注意点号) - 彻底丢弃原始文件名,不拼接、不信任任何含
.或/的部分
finfo_file() 是唯一靠谱的 MIME 校验方式
$_FILES['file']['type'] 是浏览器填的,改个 text/plain 就绕过,完全不可信。真正能反映文件本质的是 magic bytes,得靠 finfo_file() 服务端解析。
ThinkPHP 6 的 $file->getMime() 底层调用 finfo,可用,但得先确认扩展是否启用:extension_loaded('fileinfo')。
立即学习“PHP免费学习笔记(深入)”;
- 必须用
$file->getRealPath()传给finfo_file(),不能传临时路径或$_FILES['file']['tmp_name'](可能已被清理) - 校验值应严格匹配白名单:
in_array($mimeType, ['image/jpeg', 'image/png', 'image/gif']) - 图片类业务建议叠加
exif_imagetype():它更快、更轻量,且能识别常见图像格式 - 某些服务器禁用
finfo扩展,没这个函数就直接拒传,别 fallback 到$_FILES['file']['type']
重命名不是加个时间戳就行
用 date('YmdHis') . '.jpg' 还是可能被预测、覆盖或撞库;更糟的是没清洗原始名里的路径字符,导致 ../../.htaccess 直接写进上传目录。
ThinkPHP 的 moveTo() 接收绝对路径,务必确保目标不在 public/ 下——推荐 runtime/upload/ 或 /var/www/uploads/ 这类 Web 不可达路径。
- 强制重命名:用
uniqid() . '_' . mt_rand(1000, 9999) . '.' . $safeExt - 清洗原始名:
str_replace(['.', '/', '\'], '_', $filename),再取basename() - 目标路径必须是绝对路径,且父目录权限为
755,属主不是www-data(避免写入可执行脚本) - 如果必须提供访问,走 PHP 输出:
readfile($realPath); header('Content-Type: image/jpeg');,别暴露物理路径
Web 服务器配置才是最后一道墙
PHP 层校验再严,只要上传目录允许执行 PHP,攻击者绕过一次就全盘失守。这不是“补充”,是硬性依赖。
Nginx 示例:location ~ ^/uploads/.*.(php|phtml|phar)$ { return 403; };Apache 示例:php_flag engine off 放在上传目录的 .htaccess 中。
- 上传目录禁止执行 PHP,不是“最好有”,是“必须关”
- Linux 权限检查:
ls -ld /path/to/upload,确保不是777,也不属于 Web 进程用户 - 不要把上传目录放在
public/下;若已放,必须加 Web 服务器级后缀拦截 - 临时文件(
tmp_name)上传后立即移动,别留着做二次处理——move_uploaded_file()本身具备原子性和来源校验
最常被忽略的一点:MIME 校验和重命名必须在同一个请求周期内完成,不能先保存再校验。一旦文件落盘,哪怕几毫秒,都可能被利用。



















