ThinkPHP 5.1+ 中 input() 和 Request::param() 默认不自动过滤路径穿越,需手动用 urldecode()、realpath() 和 strpos() 校验路径是否在允许目录内,上传文件名必须用 basename() 截断。

ThinkPHP 5.1+ 中 input() 和 Request::param() 默认不自动过滤路径穿越
ThinkPHP 不会对用户传入的文件路径类参数(如 filename、path、file_id)做自动的 ../ 过滤或规范化。它只负责接收和解析,后续是否安全,完全取决于你是否手动校验。
常见错误写法:
$file = input('get.filename');
return readfile('./uploads/' . $file); // 危险!$file 可为 ../../etc/passwd
- ThinkPHP 的
input()是“原样透传”,不会删../,也不会调用realpath()或basename() - 即使开启
app_debug = false,该行为也不变 - 5.0/5.1/6.0/6.3 各版本均如此,无例外
手动校验路径必须用 str_replace() + realpath() 组合
仅用 str_replace(['..', './'], '', $path) 是无效的——攻击者可构造 ....//、%2e%2e/、..\..\ 等绕过。
正确做法是:先解码、再标准化、最后比对根目录前缀:
立即学习“PHP免费学习笔记(深入)”;
$raw = input('get.file');
$path = urldecode($raw);
$real = realpath('./uploads/' . $path);
if (false === $real || 0 !== strpos($real, realpath('./uploads/'))) {
throw new HttpException(403, 'Invalid file path');
}
return readfile($real);
-
realpath()会自动解析所有../、./、多重斜杠、编码绕过,返回真实绝对路径 - 必须用
strpos()检查是否在允许目录内,不能只判断是否包含../ - 注意:
realpath()对不存在的路径返回false,需提前处理或配合is_file()
上传文件名必须用 basename() 强制截断
用户通过表单上传时,$_FILES['file']['name'] 可被伪造为 ../../etc/shadow,ThinkPHP 的 UploadFile 或 think\File 类默认不清理文件名。
安全写法:
$file = request()->file('file');
$originalName = basename($file->getInfo('name'));
$safeName = md5(time() . $originalName) . '.' . $file->extension();
$file->move('./uploads/', $safeName);
-
basename()是唯一可靠截断路径的方式,pathinfo()的basename字段仍可能含恶意内容 - 不要信任
$file->getOriginalName(),它直接反射客户端传值 - 即使用了
validate(['ext' => 'jpg,png']),也无法阻止路径穿越,扩展名校验和路径校验必须分开做
配置 open_basedir 是最后一道防线,但不能替代代码校验
PHP 的 open_basedir 能限制脚本可访问的文件系统路径,但它只对 PHP 原生函数(如 fopen、file_get_contents)生效,对 ThinkPHP 自己封装的 Db::query() 或模板渲染无约束力。
在 php.ini 或 .user.ini 中设置:
open_basedir = /www/wwwroot/myapp/:/tmp/
- 必须以
/结尾(除最后一个路径),否则/www/wwwroot/myapp会被视为允许访问/www/wwwroot/myapp_bak - 若使用 Swoole 或 RoadRunner,
open_basedir可能不生效,此时代码层校验更关键 - 它无法防止日志写入、数据库写入等非文件读取类路径穿越(如日志中注入
../写入 shell)
路径穿越漏洞的关键不在“能不能绕过框架”,而在于“你有没有在业务逻辑里主动把用户输入当路径用”。只要涉及拼接路径,就必须走 realpath() + 前缀比对 流程,没有捷径。


















