近期披露的 ThinkPHP 多语言漏洞,其实并不是全新的安全问题,但在实际项目开发中,仍有不少团队因为实现方式不当而埋下风险。这个漏洞的关键点并不在于多语言功能本身会执行代码,而是在于 lang 参数参与文件包含路径拼接时缺少严格校验,导致攻击者可以加以利用,最终触发远程命令执行(RCE)。

通俗一点说,攻击者能够借助目录穿越手法(例如传入 lang=../../../../usr/local/lib/php/pearcmd),成功包含服务器中已有的系统脚本,比如 pearcmd.php,随后再借助 config-create 命令写入并执行恶意 PHP 文件。这并非纸面上的推演,而是现实中被广泛利用过的高危 ThinkPHP 漏洞利用方式。
language参数为什么能导致任意代码执行
问题的根源,出在动态加载语言包时的路径拼接逻辑。典型代码通常如下:
$langFile = $this->app->getLangPath() . $langSet . '.php';
include $langFile;
如果$langSetGET?lang=zh-cn),那么攻击者只需要传入 lang=../../../../public/pearcmd,就可能一路目录穿越到服务器上的任意 PHP 文件,并将其当成语言包进行执行。
需要特别指出的是,这类问题在 ThinkPHP 5.x 早期版本中更为常见,核心原因就是对 lang 参数缺乏白名单限制。也就是说,并不是所有 ThinkPHP 多语言调用场景都有风险——真正危险的是那些手动拼接路径后,直接使用 include 或 require 的写法。如果采用框架提供的 Lang::get() 方法,通常是相对安全的。
如果系统已经遭到攻击,日志中往往会留下比较明显的迹象:大量带有 ../ 的路径在 Warning: include(...): failed to open stream 中反复出现。更典型的现象是,访问 /index.php?lang=../../../../etc/passwd%00 这类 URL 时,竟然返回了系统文件内容——这正是文件包含漏洞的典型特征。
input('lang')必须加正则白名单,不能只靠后缀过滤
很多开发人员的第一反应,是使用 basename() 去除路径分隔符,或者简单判断参数是否以 .php 结尾。但实际情况是,这种防护远远不够。比如:basename('a/../../etc/passwd') 返回的依然是 passwd,并不能真正阻止路径穿越。还有更隐蔽的绕过方式,例如使用 lang=en_US 和 lang=en%5fUS(URL 编码下划线),单纯依赖字符串匹配很容易失效。
那么,ThinkPHP 多语言参数到底应该如何校验?行业内经过验证、较为有效的做法主要包括以下几点:
- 使用正则白名单严格匹配语言标识,例如
/^[a-z]{2}(_[A-Z]{2})?$/,只允许en、zh_CN、pt_BR这类标准语言格式 - 校验逻辑必须在中间件或控制器入口处就完成拦截,不能等进入 Lang 类后再处理——因为在执行
include之前,就应该把非法输入直接拒绝 - 限制参数长度,建议不要超过 10 个字符,避免超长 payload 引发额外的解析异常或边界问题
- 明确禁止任何点号(
.)、斜杠(/)、空格以及 Unicode 控制字符,降低文件包含与协议利用风险
绝对路径校验比basename()更可靠
即使参数已经通过正则白名单检查,也不意味着可以高枕无忧。在真正加载语言包之前,还需要再做一步路径合法性校验。这里比较稳妥、也更符合安全实践的推荐方案是:
先通过 realpath() 获取真实路径,再结合 str_starts_with() 判断目标文件是否确实位于预期的 lang/ 目录之内:
if (!str_starts_with(realpath($langFile), realpath($this->app->getLangPath()))) {
throw new HttpException(403);
}
需要注意,尽量不要使用 dirname(__FILE__) 进行路径拼接,因为一旦工作目录发生变化,这类校验逻辑可能就会失效。另外,生产环境中必须关闭 allow_url_include,否则攻击者还可能通过 lang=php://filter/... 这类方式发起协议包装器攻击。
删掉pearcmd.php不等于漏洞修复
这里必须强调一点:很多团队在发现 ThinkPHP 多语言漏洞被利用后,第一反应就是删除 pearcmd.php。这种处理方式可以理解,但从安全修复角度看,本质上只是封堵了其中一个利用入口。只要语言参数缺乏严格校验这一根本问题没有解决,攻击者依旧可以上传恶意 PHP 文件,或者利用服务器中其他现成工具——例如 phpunit、composer 自带的调试脚本——继续实现 RCE。
真正需要做到的,是彻底切断“用户可控输入 → 动态文件包含”这条攻击链。路径白名单、绝对路径校验以及生产环境清理工具文件,这三个安全环节缺一不可。还有一个容易被忽略的细节:如果路由变量采用的是 lang/:set 这种形式,它走的是 Route 路径解析流程,不会经过 Request 过滤,因此必须额外补充单独的参数校验处理。
