Lang::load()因未校验lang参数导致目录穿越漏洞,根源在于直接拼接用户输入构造路径且未用realpath()标准化和白名单约束;触发需满足config/lang.php中'switch'=>true、启用LoadLangPack中间件及框架≤6.0.13;升级至6.0.14+可修复,否则须禁用动态切换或加白名单,并在入口层熔断恶意参数。

Lang::load() 未校验 lang 参数导致目录穿越
漏洞根源不在“用不用多语言”,而在 Lang::load() 直接拼接用户传入的 lang 值构造文件路径,且未做 realpath() 标准化和白名单约束。例如请求 ?lang=../../../../etc/passwd,最终加载路径变成 /var/www/html/lang/../../../../etc/passwd.php → /etc/passwd。
关键触发条件有三个:config/lang.php 中 'switch' => true、启用了 thinkmiddlewareLoadLangPack::class、且框架版本 ≤ 6.0.13。
- 不要依赖前端不传恶意值——
lang可从 GET/POST/HEADER/Cookie 任意位置注入 - 即使语言包目录是
app/lang,攻击者也能靠../跳出并加载任意 PHP 文件(如/usr/local/lib/php/pearcmd.php) -
include或require加载非语言文件时,PHP 会直接执行其中的代码,形成 RCE
升级到 6.0.14+ 是最简修复方式
官方已在 6.0.14 中重写 Lang::load() 的路径处理逻辑,强制调用 realpath() 并校验是否落在 lang/ 目录下。但必须注意 Composer 版本约束写法,否则升级失败。
- 运行
composer update topthink/framework:^6.0.14—— 不能只写^6.0或~6.0.0,否则仍锁在 6.0.13 - 升级后立即执行
php think version,确认输出为v6.0.14或更高 - 检查
config/lang.php中'path'配置是否为绝对路径(如__DIR__ . '/../lang'),相对路径会削弱realpath()校验效果
无法升级时必须禁用动态切换或加白名单
若因兼容性问题无法升级,不能只“注释中间件”了事——有些项目通过手动调用 Lang::setLocale() 或路由参数触发加载,仍可能绕过。
立即学习“PHP免费学习笔记(深入)”;
- 将
config/lang.php中'switch' => false,彻底关闭运行时语言参数解析 - 若业务强依赖切换,需在中间件或全局过滤器中显式校验:
in_array($this->request->param('lang'), ['zh-cn', 'en-us'], true) - 禁止使用
$_GET['lang']等原始输入直接传给Lang::load();所有语言标识必须来自预设数组,不可拼接、不可反射、不可变量覆盖
入口层熔断能拦住 90% 的自动化扫描
即使框架层修复了,攻击流量仍可能打到 PHP 解析器前。Nginx 或入口文件的关键词拦截,是成本最低的兜底手段。
- 在
public/index.php顶部插入参数熔断(在define('APP_PATH', ...)后): - 遍历
['lang', 's', '_method', 'function', 'vars'],对每个键检查$_GET/$_POST值是否含eval、assert、system、exec、shell_exec、passthru等关键字,匹配则http_response_code(403); exit; - Nginx 层加
if ($args ~* "(s=|/think\app/invokefunction|call_user_func|eval\(|unserialize\()" ) { return 403; },比 PHP 层更早拦截
真正危险的不是“开了多语言”,而是把用户可控字符串当路径片段用。哪怕只改一行 realpath() + str_starts_with() 校验,也比关掉整个功能更稳妥——但前提是,你得知道哪一行该改,以及改完是否被其他地方绕过。



















