根本原因是allow_lang_list仅用于生成加载列表,不拦截非法输入;lang参数未校验即拼接路径,须在LoadLangPack执行前的手动中间件中强制白名单校验并标准化格式。

配置 allow_lang_list 后语言切换仍允许非法值(比如 zh_CN、ja、../../etc/passwd),根本原因是框架未在关键路径做白名单校验——它只用这个配置来生成语言包加载列表,但不拦截非法输入。
lang 参数没被白名单过滤就直接拼路径
ThinkPHP 6.x 默认把 input('lang') 或 URL 中的 l 参数原样拼进语言包路径,例如 app/lang/ + input('lang') + /common.php。即使 config/lang.php 里写了 'allow_lang_list' => ['zh-cn', 'en-us'],框架也不会主动拒绝 zh_CN 或 ../ 这类值。
- 现象:访问
/index/index?l=zh_CN,页面不报错,但语言包加载失败,lang('hello')返回原 key - 更危险的是:
?l=../../../../etc/passwd在 6.0.13 及以下版本可能触发目录遍历 - 原因:框架只在
LoadLangPack中途调用Lang::setLangSet(),但该方法不校验输入是否在allow_lang_list内
必须手动在中间件里做白名单校验
不能依赖配置项自动生效,得在 app/middleware.php 的全局中间件或自定义中间件中提前拦截。校验必须在 Lang 或 LoadLangPack 执行前完成。
- 从请求中取 lang 值:
$lang = $request->param('l', $request->cookie('think_lang', 'zh-cn')) - 强制转小写并标准化格式:
$lang = strtolower(str_replace('_', '-', $lang))(把zh_CN→zh-cn) - 白名单比对:
in_array($lang, config('lang.allow_lang_list', [])),不通过就抛异常或 fallback - 设为合法语言:
Lang::setLang($lang),不是Lang::setLocale()(后者不改默认语言上下文)
为什么 Lang::setLocale() 不起作用
这个方法只影响后续 Lang::get() 调用的语言环境,但不会修改 config('lang.default_lang'),也不触发语言包重载。而 LoadLangPack 中间件初始化时已按 default_lang 加载了语言文件,此时再调 setLocale 已晚。
立即学习“PHP免费学习笔记(深入)”;
- 典型失败场景:控制器里写
Lang::setLocale('en-us'); echo lang('hello');→ 仍输出中文 - 真正起效的位置:必须在
app/middleware.php全局中间件的handle()最开头 - 正确写法:
Config::set('lang.default_lang', $lang);,再跟Lang::setLang($lang)
allow_lang_list 配置本身要避免大小写陷阱
Linux 系统下文件路径严格区分大小写,allow_lang_list 里的值必须和实际语言包目录名完全一致——全小写、短横线分隔、无多余字符。
- 错误写法:
['zh_CN', 'en_US']→ 框架会尝试加载app/lang/zh_CN/common.php,但文件实际是zh-cn.php,静默失败 - 正确写法:
['zh-cn', 'en-us'],且确保对应目录存在:app/lang/zh-cn/common.php - 额外建议:在
app/lang/下加个空的index.html,防止目录被意外列出来
最易被忽略的一点:白名单校验必须在 LoadLangPack 中间件执行之前完成。一旦它先跑起来,非法 lang 值就会被当作有效语言去加载不存在的路径,既不报错也不 fallback,只会让所有 lang() 调用返回原 key —— 表面看“好像能切”,实则翻译全部失效。



















