本文目录导读:

PHP 上传漏洞通常涉及文件类型绕过、路径穿越、文件内容恶意以及服务器配置缺陷,修复需要从代码层面和服务器配置层面双管齐下。
以下是系统的修复方案,按严重程度和优先级排序:
第一步:代码层面修复(核心)
严禁使用客户端 MIME 类型校验(最容易被绕过)
不要信任 $_FILES['file']['type'] 和 $_FILES['file']['name'] 的后缀名,这些皆可被伪造。
使用服务端白名单校验(必须做)
错误的做法: 黑名单(如禁止 .php、.asp),因为容易漏掉 .phtml、.php5、.htaccess 等。
正确的做法: 只允许特定扩展名(白名单)。
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif'];
$extension = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $allowed_extensions, true)) {
die('非法文件类型');
}
校验文件真实内容(绕过后缀名欺骗)
仅检查扩展名不够,攻击者可能把 PHP 代码嵌入图片中,需要使用 getimagesize() 或 finfo 检查文件头。
// 推荐使用 Fileinfo 扩展
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
$allowed_mime = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($mime, $allowed_mime, true)) {
die('文件内容不合法');
}
⚠️ 特别提醒: 如果是图片上传,处理完校验后,建议使用 GD 库或 Imagick 重新重绘图片(去除隐含的 webshell 代码)。
重命名文件(彻底切断执行通道)
用户上传的文件必须重命名,不允许使用用户提供的原始文件名。
// 生成随机文件名,掐断扩展名可控的路径
$new_name = uniqid('img_', true) . '.' . $extension;
存储到独立目录,并禁止执行权限
将上传文件放到 Web 根目录之外,并通过单独的脚本(如 download.php)读取输出;如果必须在 Web 目录下,确保该目录下 php 引擎不可解析。
防止路径穿越
不要直接把文件名拼接进路径,使用 basename() 过滤,或者像第 4 点一样重命名。
第二步:服务器配置层面修复(零容忍)
目标: 即使黑客上传了 shell.php,服务器也不执行它。
Nginx + PHP-FPM 配置
确保在 nginx.conf 的 location 块中,明确指定上传目录不允许解析 PHP。
# 假设上传目录是 /uploads/
location ~* ^/uploads/.*\.(php|php5|phtml)$ {
deny all; # 或者 return 403;
}
Apache 配置
在 <Directory> 指令中,或者放入 .htaccess(虽然不推荐用 .htaccess,但若无法修改主配置可用):
<FilesMatch "\.(?i:php|php5|phtml|phar)$">
Require all denied
</FilesMatch>
放在上传目录的 .htaccess 中,禁止一切 PHP 执行:
php_flag engine off RemoveHandler .php .phtml .php5
禁用危险的 PHP 函数(纵深防御)
在 php.ini 中,强烈建议关闭危险函数,即使文件被上传成功,也无法执行系统命令:
disable_functions = system, exec, passthru, shell_exec, popen, proc_open, pcntl_exec, ini_alter, symlink, allow_url_fopen
第三步:进阶防御措施
文件大小限制
限制 post_max_size 和 upload_max_filesize,防止上传超大文件消耗资源导致 DoS。
随机目录存储
不要把所有文件放在同一个目录,按日期或随机数分目录存储,增加攻击者猜测路径的难度。
文件名 & 路径二次检查
使用 realpath() 检查最终存储路径是否越界。
第四步:紧急修复万能脚本(快速防范)
如果你已经存在漏洞且被攻击过,临时处理可以这样拦截:
// 在 upload 处理文件的最顶端加这一段逻辑
$bad_ext = ['php', 'php3', 'php4', 'php5', 'phtml', 'pht', 'phar', 'asp', 'aspx', 'jsp'];
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (in_array($ext, $bad_ext)) {
http_response_code(403);
exit('Access Denied');
}
// 同时检查 Header 和伪造的 GIF89a 图片头
$content = file_get_contents($_FILES['file']['tmp_name']);
if (preg_match('/<\?php/i', $content)) {
exit('非法内容');
}
总结清单(自查表)
| 检查项 | 是否已做 |
|---|---|
| 服务端扩展名白名单校验 | ✅ |
finfo MIME 内容校验 |
✅ |
| 文件重命名(不用原始文件名) | ✅ |
| 上传目录禁止解析 PHP | ✅ |
| 图片二次渲染(重绘) | ✅ |
disable_functions 配置 |
✅ |
| 目录外存储 / 随机目录 | ✅ |
最核心原则: 存储与执行分离,你的代码无论过滤得多么严密,只要最终文件落在 Web 根目录且可执行,就存在风险。确保服务器不执行上传目录中的任何脚本文件是最终的底线。