最稳妥的方案是后端白名单校验后缀、重命名文件,并在服务器层面禁止上传目录执行 PHP 脚本。
先说结论:单纯依赖后端后缀校验不够,必须结合服务器配置禁止上传目录的脚本执行权限,才能有效防止 Webshell。
- 先判断:确认业务是否真的允许上传非图片类文件,能禁则禁。
- 优先做:代码层强制白名单校验 + 文件重命名,不使用原始文件名。
- 再验证:尝试上传 .php/.phtml 等后缀,确认无法访问或执行。
快速处理思路
如果不方便立即修改服务器配置,先在代码层做严格限制。ThinkPHP 6 中建议使用 request()->file() 链式调用进行校验,示例如下:
$file = request()->file('file');
if ($file) {
// 强制白名单校验扩展名
$info = $file->validate(['ext' => 'jpg,png,gif'])->move('upload');
// 务必重命名,不要保留原文件名
}注意:仅靠代码校验无法完全防御解析漏洞,如果服务器没有禁止上传目录的 PHP 执行权限,攻击者仍可能通过特殊后缀或配置绕过。
核心风险原理
文件上传漏洞的核心在于服务器“信任”了用户上传的文件内容或后缀。常见的绕过方式有三种:
- 后缀绕过:后端只检查了后缀名,但服务器配置允许解析 .php5、.phtml、.phar 等冷门后缀,或者存在大小写绕过(Windows 环境)。
- 内容绕过:文件后缀是 jpg,但文件头包含了 PHP 代码,如果服务器配置不当,可能将其当作 PHP 执行。
- 路径绕过:利用上传目录的遍历漏洞,将文件保存到可执行目录。
因此,单靠代码校验后缀是不够的,必须配合服务器权限控制,形成纵深防御。
分步落地实施
第一步:代码层白名单校验与完整示例
在 ThinkPHP 控制器中,不要使用黑名单,必须使用白名单。只允许业务需要的后缀。以下是包含验证、移动、重命名的完整控制器方法示例:
public function upload()
{
$file = request()->file('file');
if (!$file) {
return json(['code' => 0, 'msg' => '未检测到上传文件']);
}
// 1. 强制白名单校验扩展名
$info = $file->validate(['ext' => 'jpg,png,gif'])->move('upload');
if (!$info) {
return json(['code' => 0, 'msg' => $file->getError()]);
}
// 2. 强制重命名文件(hash 策略生成随机文件名)
$savename = \think\facade\Filesystem::disk('public')->putFile('topic', $file, 'hash');
return json(['code' => 1, 'url' => $savename]);
}第二步:服务器禁止上传目录执行 PHP
这是最关键的一步。在 Nginx 或 Apache 配置中,明确禁止上传目录解析 PHP 脚本。
Nginx 配置示例:
# 注意:/upload/ 路径需根据 config/filesystem.php 中的实际目录修改
location ~* /upload/.*\.(php|php5|phtml|php7)$ {
deny all;
}Apache 可在上传目录放置 .htaccess:
<FilesMatch "\.(php|php5|phtml)$">
Order Deny,Allow
Deny from all
</FilesMatch>验证与排查
完成配置后,不要直接上线,先在测试环境验证。
- 尝试上传一个内容为
<?php phpinfo();?>的文件,后缀改为 .php。观察系统是否拦截。 - 如果系统允许上传(例如后缀被改成 .jpg),尝试在浏览器访问该文件 URL。如果显示空白或下载,说明服务器未执行 PHP;如果显示 phpinfo 信息,说明服务器配置未生效,存在风险。
- 检查上传目录的权限,确保 Web 服务器用户只有写入权限,没有执行权限。
常见坑点
- 前端校验不可信:JavaScript 做的后缀检查只能防误操作,攻击者可以绕过前端直接发包,后端必须二次校验。
- 大小写与空格:Windows 服务器对大小写不敏感,.Php 可能也能执行;部分旧版本 PHP 对文件名末尾的点或空格处理不当,需确保存储时清理了这些字符。
- 二次渲染缺失:如果是图片上传,最好经过 GD 库或 ImageMagick 二次渲染,这样可以去除图片中嵌入的恶意代码。
- 独立域名存储:条件允许的话,将上传文件存储到独立的静态域名或 OSS 对象存储,彻底隔离脚本执行环境。