thinkphp 5.1+ 路径穿越防护必须用 realpath() + 前缀校验,仅 basename() 无效;需 urldecode() 处理编码绕过、正则白名单过滤、上传文件强制重命名、禁用用户路径拼接。

ThinkPHP 5.1+ 默认不处理路径穿越,input() 和 Request::param() 返回原始字符串——你直接拼进 file_get_contents() 或 unlink(),就等于把服务器文件系统交到攻击者手上。
为什么 basename() 单独用根本挡不住目录遍历
很多人在控制器里写 $file = basename(input('get.file')),以为这就安全了。其实它只剥离斜杠前的内容,对 ../../../etc/passwd%00.jpg 返回 passwd%00.jpg,后续若再用 pathinfo($file, PATHINFO_EXTENSION) 判断类型,%00 后的内容会被忽略,导致扩展名校验失效;更糟的是,如果拼路径后直接传给 file_exists(),空字节可能被截断,绕过检查。
所以 basename() 只是第一步,不是终点。必须搭配:
- 立即调用
urldecode()处理 URL 编码绕过(如%2e%2e%2f) - 用正则白名单过滤文件名:
preg_match('/^[a-zA-Z0-9_.-]+$/', $safe_name) - 绝不依赖它做路径合法性判断——它不解析
..,也不展开符号链接
realpath() + 前缀校验才是唯一可靠防线
realpath() 会真正解析所有 ../、./、多重斜杠、符号链接和编码变体,返回操作系统中的真实绝对路径。但它必须配合严格前缀比对,否则毫无意义。
正确写法示例:
$root = realpath('./uploads/');
$raw = input('get.file');
$path = urldecode($raw);
$full = realpath('./uploads/' . $path);
if (false === $full || 0 !== strpos($full, $root . '/')) {
throw new HttpException(403, 'Invalid file path');
}
return readfile($full);
注意几个关键点:
-
$root末尾必须带斜杠,否则/var/www/uploadsssh_key可能被误判通过 - 不能用
strpos($full, $root) !== false—— 它对false返回0,造成误判 -
realpath()对不存在路径或越权路径返回false,这个值必须显式检查,不能跳过 - 校验必须在
file_exists()或is_readable()之前完成,否则存在竞态条件风险
上传场景下 $_FILES['file']['name'] 的特殊处理
ThinkPHP 的 think\File 类或原生 $_FILES 不会自动清理 name 字段,攻击者可伪造 ../../.env 直接传入。这时候光靠 basename() 还不够。
必须做到:
- 不用
$_FILES['file']['name']拼目标路径,哪怕加了basename() - 用
pathinfo($name, PATHINFO_FILENAME)和pathinfo($name, PATHINFO_EXTENSION)分离原始名与扩展名 - 强制重命名:如
uniqid() . '_' . md5($original_name) . '.' . $ext - 保存目录权限设为
0750,属主为 Web 进程用户,避免其他账户读取
别让业务逻辑暴露路径结构
最安全的方案,是根本不要让用户传路径。比如下载接口,接收 $id,查数据库映射到物理路径:/var/uploads/<code>sha256($id)/report.pdf。这样既规避所有路径解析风险,又天然支持权限控制、状态校验和审计追踪。
如果你还在拼接 ./uploads/ + 用户输入,哪怕加了五层校验,只要有一处漏掉 urldecode()、或 $root 少了个斜杠、或没检查 realpath() 返回 false,整条防线就形同虚设。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











