ThinkPHP的_()或lang()函数默认不转义,因语言包机制是纯字符串替换、不区分输出上下文,自动转义会破坏JS/JSON等非HTML场景,故由开发者按上下文显式调用htmlspecialchars($str, ENT_QUOTES, 'UTF-8')等安全处理。

ThinkPHP 多语言场景下,_() 或 lang() 返回的翻译字符串**默认不转义**,直接输出到 HTML 会导致 XSS 风险或标签解析异常——你得自己加一层 htmlspecialchars()。
为什么 _() 不自动转义
ThinkPHP 的语言包机制本质是字符串替换,它不区分上下文:同一段翻译可能用于 HTML 输出、JS 字符串、JSON 值甚至 SQL 拼接。自动转义会破坏非 HTML 场景(比如 title 属性里多转一次引号就错),所以框架选择“零假设”,交由开发者按需处理。
- 语言包内容本身不应含未转义的 HTML 标签(如
<b>重要</b>),否则_()返回的就是带标签的字符串,需用htmlspecialchars()还是htmlspecialchars_decode()取决于你是否想渲染它 - 若语言项中写了
'name' => '用户 <script>alert(1)</script>',_()直接返回该字符串,不拦截也不警告 - 模板中写
<h2><?php echo _('welcome'); ?></h2>是危险的,等价于裸 echo
htmlspecialchars() 怎么配才安全
在 ThinkPHP 中调用 htmlspecialchars() 必须显式指定 ENT_QUOTES 和 UTF-8,否则单引号不转、编码不匹配会导致绕过。
- 正确写法:
htmlspecialchars(_('welcome'), ENT_QUOTES, 'UTF-8') - 错误写法:
htmlspecialchars(_('welcome'))(缺参数,PHP 8+ 会警告;且默认不转单引号) - 别用
htmlentities()替代——它会把中文也转成你好,可读性差,且无额外安全收益 - 如果翻译内容本就含 HTML(比如富文本提示),应改用白名单过滤(如
strip_tags($str, '<b><i><u>'))而非全转义
模板里怎么写才不漏转
ThinkPHP 模板引擎不提供内置转义过滤器(如 Laravel 的 {{ }} vs {!! !!}),所以必须手动包裹。
立即学习“PHP免费学习笔记(深入)”;
- 推荐封装一个助手函数:
function h($str) { return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); },然后模板中写<?php echo h(_('welcome')); ?> - 避免在模板里重复写长函数调用,更不要用
echo htmlspecialchars(_(...), ...)套三层 - 注意:Blade-like 的
{:__('welcome')}语法(来自某些扩展)也不自动转义,仍需{:h(__('welcome'))} - JS 中使用语言变量时,不能直接
var msg = "<?php echo _('error'); ?>";——必须先json_encode()或htmlspecialchars()+addcslashes()处理换行和引号
多语言与 URL 参数混用时的陷阱
当翻译内容被拼进 URL(如分页链接中的提示文字),或作为 data-* 属性值,单纯 htmlspecialchars() 不够——它不编码空格、斜杠等 URI 特殊字符。
- 拼链接时:
$url = '?msg=' . rawurlencode(_('not_found'));,不是urlencode()(它把空格变+,不可靠) - 作
data-tip属性:<div data-tip="<?php echo htmlspecialchars(_('click_me'), ENT_QUOTES, 'UTF-8'); ?>">,双引号属性必须确保内部引号被转义 - 切勿对已转义过的字符串再次调用
htmlspecialchars(),否则&变成&,页面显示为字面量 - 语言包里的键名(如
'user_name')若用于动态 SQL 字段,和多语言无关,但常被误认为“已安全”——字段名永远不能靠转义防注入,必须白名单校验
最易被忽略的一点:语言包文件本身是 PHP 数组或 INI/JSON,若用脚本自动生成,要防止原始翻译数据里混入未清理的 HTML 或 JS 片段;一旦入库,后续所有 _() 调用都继承风险,比代码里漏转更难排查。



















