thinkphp 5.1+ 默认不拦截路径穿越,input()和request::param()返回原始字符串,必须手动用basename()、urldecode()、realpath()及strpos()校验是否在授权目录内,仅str_replace或basename不足以防御绕过。

ThinkPHP 5.1+ 默认不拦截 ../,input() 和 Request::param() 返回的路径类参数(如 filename、path)是原始字符串,不做任何规范化或白名单校验——你直接拼进 file_get_contents() 或 unlink(),就等于把服务器文件系统裸奔交给攻击者。
用 basename() 截文件名只是第一步,不是终点
很多人以为 basename($_GET['file']) 就安全了,其实它只解决“字符串里带斜杠”的表层问题。比如用户传 ../../../etc/passwd%00.jpg,basename() 返回 passwd%00.jpg,后续若用 file_exists() 判断再拼路径,空字节可能被截断导致绕过;更危险的是,如果业务逻辑里还依赖扩展名做判断(如 pathinfo($name, PATHINFO_EXTENSION)),%00 后的内容会被忽略,造成类型校验失效。
所以必须搭配后续校验:
-
basename()之后,仍要对结果做字符白名单过滤,例如只允许字母、数字、下划线、短横线和点号:preg_match('/^[a-zA-Z0-9_.-]+$/', $safe_name) - 禁止用户控制完整路径结构,上传/下载/删除场景一律用固定目录 + 随机哈希名(如
uniqid() . '_' . md5($original)) - 若必须保留原始文件名,
basename()后立即调用urldecode()一次,防止%2e%2e%2f等编码绕过
realpath() + 白名单根目录前缀校验才是核心防线
仅靠字符串检测(如 strpos($input, '..'))完全不可靠——攻击者可用 ....//、%2e%2e%2f、..\(Windows)、Unicode 归一化等手法绕过。真正有效的做法是让 PHP 解析出真实路径,再确认它是否落在授权范围内。
正确写法示例:
$uploadRoot = realpath('./public/uploads');
$requested = realpath('./public/uploads/' . basename($_GET['file']));
if ($requested === false || strpos($requested, $uploadRoot) !== 0) {
abort(403, 'Invalid file path');
}
if (is_file($requested) && is_readable($requested)) {
return response()->download($requested);
}
注意几个关键点:
-
$uploadRoot必须以/结尾(如'/var/www/html/public/uploads/'),否则/uploads2/evil.txt可能被误判为合法 -
realpath()在open_basedir限制外会返回false,这反而是好事——别把它当成“文件不存在”处理,应统一拦截 - 不要用
str_starts_with($requested, $uploadRoot)替代strpos(..., ...) === 0,PHP 8.0+ 才支持前者,老环境会报错
ThinkPHP 特有风险:路由参数和隐式路径拼接
ThinkPHP 的 url_route_must = false(默认)会让框架在匹配失败时尝试隐式解析,比如把 /index.php?s=/../../etc/passwd 当成控制器路径处理;更隐蔽的是,某些插件或自定义中间件可能直接拿 Request::pathinfo() 拼路径,而没走 input() 流程。
必须检查这些位置:
- 所有用到
Request::pathinfo()、input('s')、input('route')的地方,都要加basename()+realpath()校验 -
config/route.php中确认'url_route_must' => true已开启,否则Route::miss()不生效 - 禁用
url_common(config/app.php中设为false),防止?s=xxx绕过路由规则
最易被忽略的是:ThinkPHP 的上传类(think\File)虽自动调用 basename(),但如果你在 moveTo() 前手动拼接了自定义子目录(如 $file->move($root . '/' . $_POST['subdir'])),那个 subdir 就成了新的遍历入口——它不会被上传类保护,必须单独校验。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











