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

ThinkPHP 默认不拦截路径遍历,所有通过 input()、Request::param() 获取的路径类参数(如 file、view、template)都是原始字符串——你直接拼进 include、file_get_contents() 或 think\facade\View::fetch(),就等于把服务器文件系统交到攻击者手上。
为什么 basename() 单独用不安全
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() 校验。
realpath() + strpos() 是唯一可靠路径校验组合
仅靠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'); }
• $root 必须以 / 结尾,否则 /app/views2/xxx 会被误判为合法
• realpath() 对不存在路径返回 false,不能当成“文件不存在”忽略,必须统一拦截
• 别用 str_starts_with() —— PHP 8.0+ 才支持,老环境直接报错
• View::fetch() 若传入用户参数,必须先走这套校验,不能直接透传
上传和下载场景必须切断路径拼接
用户上传的$_FILES['file']['name'] 和下载接口的 filename 参数,永远不要直接拼:
• 不要写 file_get_contents('./uploads/' . $_GET['filename'])
• 不要用 header('Content-Disposition: attachment; filename="' . $_GET['filename'] . '"') 直接输出用户输入
• 下载时应只允许预设 ID(如 download?id=123),查数据库映射到内部安全路径
• 上传文件名必须重命名(如 UUID + 时间戳),原始 name 字段仅作展示,绝不参与路径构造
Lang::load() 或 Config::load() 的调用点,就等于在防火墙上凿了个洞。php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











