ThinkPHP不拦截路径遍历,basename()仅截字符串,无法防御%00、编码绕过等攻击;必须配合urldecode()、realpath()和strpos()校验真实路径是否在授权目录内。

ThinkPHP 本身不拦截路径包含,所有 input()、Request::param() 返回的路径类参数(如 file、view、template)都是原始字符串——你直接拼进 include、file_get_contents() 或 think\facade\View::fetch(),就等于把服务器文件系统交到攻击者手上。
为什么 basename() 单独用不安全
很多人以为 basename($_GET['file']) 就万事大吉,其实它只做字符串截断,完全不管编码、空字节、符号链接或协议伪路径:
-
../../../etc/passwd%00.jpg经basename()变成passwd%00.jpg,后续若用file_exists()判断再拼路径,旧版 PHP 可能被%00截断,导致校验失效 -
http://evil.com/shell.php经basename()仍得shell.php,但include()会直接远程加载 -
.//./../config.php这类多重斜杠 + 点号组合,basename()不解析也不清理,原样保留
所以 basename() 只能作为第一步:强制剥离路径结构,之后必须配合白名单字符过滤 + realpath() 校验。
realpath() + strpos() 是唯一可靠路径校验组合
仅靠 str_replace(['..', './'], '', $input) 或正则删点号,100% 被绕过。真正有效的做法是让 PHP 解析出真实绝对路径,再确认它是否落在授权目录内:
立即学习“PHP免费学习笔记(深入)”;
$root = realpath('./app/views'); // 必须用 realpath() 得到真实路径,且末尾带斜杠
$requested = realpath('./app/views/' . basename(urldecode($input))); // 先 urldecode,再 basename,再 realpath
if (false === $requested || 0 !== strpos($requested, $root . '/')) {
abort(403, 'Invalid file path');
}
关键点:
-
$root必须以/结尾,否则/app/views2/xxx会被误判为合法 -
realpath()对不存在路径返回false,不能当成“文件不存在”忽略,必须统一拦截 - 别用
str_starts_with()—— PHP 8.0+ 才支持,老环境直接报错 - ThinkPHP 的
View::fetch()若传入用户参数,必须先走这套校验,不能直接透传
上传和下载场景必须切断路径拼接
用户上传的 $_FILES['file']['name'] 和下载接口的 filename 参数,永远不要直接拼路径:
- 上传时:用
basename($file->getInfo('name'))截取后,立即用preg_match('/^[a-zA-Z0-9_.-]+\.php$/i', $name)做扩展名校验,再生成随机哈希名(如uniqid() . '_' . md5($name))存盘 - 下载时:接口只接收
file_id,查数据库确认归属、状态、原始名,再用固定根目录 + 哈希物理路径提供服务;禁用任何file_get_contents('./uploads/' . $_GET['f'])类写法 - 即使用了 ThinkPHP 的
validate(['ext' => 'php']),也防不住路径穿越 —— 扩展名校验和路径校验是两件事,必须分开做
open_basedir 是边界线,不是补丁
在 php.ini 中配置 open_basedir = /var/www/myapp/:/tmp/(注意末尾斜杠),能让所有文件操作函数(include、file_get_contents、fopen)在越界时直接报错,但它不修复代码漏洞,只是兜底:
- 宝塔面板勾选“防跨站攻击”后,必须手动重启 PHP-FPM,否则配置不生效
- 别在代码里用
ini_set('open_basedir', ...)—— FastCGI 模式下基本无效 - 如果项目用了符号链接部署(如
current → releases/20260417),realpath(__DIR__)返回的是物理路径,而open_basedir配置的是逻辑路径,两者对不上就会报错 -
system()、exec()不受open_basedir限制,该禁还得禁
最危险的不是没配 open_basedir,而是配了却误以为高枕无忧 —— 它拦不住 eval($_POST['code']),也拦不住 SQL 注入后读取数据库配置文件的行为。



















