thinkphp多语言漏洞本质是lang参数未路径规范化导致目录穿越,攻击者可构造如../../../../etc/passwd等恶意值,使lang::load()加载任意文件;修复需白名单校验+realpath路径约束+禁用动态切换或升级至6.0.14/5.1.9/5.0.13以上版本。

ThinkPHP 多语言功能本身不危险,但 lang 参数若未经路径规范化就直接拼入文件加载逻辑,就会触发目录穿越——这不是“多语言该不该用”的问题,而是“怎么加载语言包”写错了。
为什么 lang 参数能导致文件包含
ThinkPHP 6.0.13 及更早版本中,Lang::load() 内部调用 think\facade\App::langPath() 拼接语言包路径时,未对用户传入的 lang 值(如来自 URL 的 ?lang=../../../../etc/passwd)做路径净化。结果就是框架试图加载 /var/www/html/lang/../../../../etc/passwd.php,等价于 /etc/passwd。
- 触发条件:多语言开关开启(
config/lang.php中'switch' => true),且请求携带恶意lang参数 - 关键漏洞点在
think\lang\Lang::load()对$lang参数的处理,而非lang()函数本身 - PEAR 组件被利用是二次攻击链,核心前提是路径穿越已成功
如何安全加载语言包(非绕过,真修复)
不能靠“前端不传 bad lang”来防御,必须从加载逻辑源头堵住。正确做法是强制语言包路径白名单化 + 路径标准化:
- 禁用动态语言切换:将
config/lang.php中'switch' => false,彻底关闭运行时语言参数解析 - 若必须支持切换,只允许预设值:
'allow_lang_list' => ['zh-cn', 'en-us'],并在中间件里手动校验$this->request->param('lang')是否在此列表中 - 重写
Lang::load()加载逻辑(或通过事件监听),用realpath()+str_starts_with()确保最终路径落在lang/目录下:if (!str_starts_with(realpath($file), realpath(config('lang.path')))) { throw new \InvalidArgumentException('Invalid lang file path'); }
验证器里用 lang() 提示时的陷阱
很多人在自定义 getRuleMsg() 里调用 lang(),以为只是读提示——但若此时 lang 参数已被污染,lang() 内部仍可能触发不安全的文件加载(尤其在旧版中未打补丁时)。
-
lang('validate.name.require')底层会尝试加载lang/zh-cn.php,但如果App::lang()返回的是攻击者控制的值(如../../../../etc/passwd),就又回到路径穿越起点 - 解决方案:永远不要把用户输入(
$_GET['lang']、$_SERVER['HTTP_THINK_LANG'])直接喂给Lang::set();改用 session 或 cookie 中由服务端可控写入的语言标识 - 检查
app/middleware.php是否注册了think\middleware\LoadLangPack—— 这个中间件默认信任detect_var,是漏洞入口之一
升级不是可选项,而是止损动作
6.0.14+、5.1.9+、5.0.13+ 已在 Lang::load() 中加入 basename() 路径截断和白名单校验。但仅升级不够:
- 确认
composer.lock中topthink/framework版本号真实生效(有些项目 lock 文件没更新) - 检查是否引入了第三方扩展覆盖了原生
Lang类,那些代码未必同步修复 - 线上应监控 Nginx 日志中含
lang=\.+且路径含..的请求,这是最直接的攻击痕迹
真正容易被忽略的,是开发者以为“我只用了 lang() 显示文字”,却没意识到这个函数背后牵着整个语言包加载链——只要加载逻辑没加固,任何调用都可能是突破口。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











