发布于2026-07-05 阅读(0)
扫一扫,手机访问
ThinkPHP 在路径处理上其实一直有个“默认不设防”的特点——input() 和 Request::param() 拿到的路径类参数(比如 file、view、template)都是原始字符串,不做任何过滤。如果你直接把这些字符串拼进 include、file_get_contents() 或 think\facade\View::fetch(),那就等于把服务器文件系统的访问权限拱手让给了攻击者。
那么问题来了:想防住路径遍历,到底该怎么操作?先看一个常见的误区。
basename() 这个函数看起来很方便——它只取路径的最后一部分。但麻烦在于,它只做字符串截断,完全不关心编码、空字节、符号链接或协议伪路径。举个例子:
../../../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() 校验才能生效。
有些开发者尝试用 str_replace(['..', './'], '', $input) 或正则表达式来删除点号,但这类做法 100% 会被绕过。真正可靠的办法是让 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'); } — 如果 realpath() 返回 false(路径不存在)或结果不在授权目录下,直接终止请求。这里有几个关键点需要注意:
$root 末尾必须加斜杠,否则像 /app/views2/xxx 这样的路径会被误判为合法。realpath() 对于不存在的路径返回 false,不能简单地当成“文件不存在”放过——必须统一拦截,因为可能是恶意构造的路径。str_starts_with(),PHP 8.0+ 才支持,老环境直接报错。用 strpos() 或 substr() 更稳妥。View::fetch() 并传入用户参数,必须先走这套校验,不能直接透传。上传时 $_FILES['file']['name'] 和下载接口的 filename 参数,是路径遍历攻击的高发区。永远不要直接把它们拼进路径里:
file_get_contents('./uploads/' . $_GET['filename']),这种代码一写就炸。header('Content-Disposition: attachment; filename="' . $_GET['filename'] . '"') 直接输出用户输入,因为文件名里可能包含路径分隔符。download?id=123),通过查数据库映射到内部安全路径。name 字段仅作展示,绝不能参与路径构造。说到底,写对几行校验代码并不难,真正难的是确保所有路径类入口——模板渲染、多语言加载、插件配置读取、日志导出——都经过同一套校验逻辑。漏掉一个 Lang::load() 或 Config::load() 的调用点,就等于在防火墙上凿了个洞。

售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8