thinkphp5模板默认不转义变量输出,{$var}直接渲染原始内容,必须在每个输出点按上下文手动处理:html文本用htmlspecialchars($var, ent_quotes | ent_html5, 'utf-8'),属性值同理,富文本须用htmlpurifier白名单过滤,default_filter不可靠,json输出前也需后端预处理。

ThinkPHP5 默认不转义模板变量输出,{$var} 直接渲染原始内容——这意味着只要用户输入了 <script>alert(1)</script>,它就会被执行。别指望 default_filter 或框架“自动防护”能兜底,必须在每个输出点按上下文手动处理。
模板里 {$var} 为什么直接执行脚本?
ThinkPHP5 的模板引擎默认关闭自动转义,{$var} 等价于 <?php echo $var; ?>,不做任何过滤。这不是 bug,是设计选择:框架无法预判你输出的位置(HTML 文本?属性值?JS 字符串?)。
- 常见错误:
<div title="{$title}">{$content}</div>—— 如果$title是abc" onmouseover="alert(1),属性就提前闭合并注入事件 - 正确写法:
<div title="{:htmlspecialchars($title, ENT_QUOTES, 'UTF-8')}">{:htmlspecialchars($content, ENT_QUOTES | ENT_HTML5, 'UTF-8')}</div> - 模板过滤器
{$var|html}可用,但它内部调用的是htmlspecialchars($var),**不带ENT_QUOTES和'UTF-8'参数**,单引号不转义,onerror='alert(1)'仍可触发 - 如果用了
{$var|raw}或{$var|default='-'|html},注意顺序:|html必须在最后,否则默认值不会被转义
控制器返回 JSON 时怎么防 XSS?
ThinkPHP5 的 json() 方法只做 PHP 数组到 JSON 字符串的编码,**完全不处理 XSS**。前端若用 innerHTML 渲染 data.content,而该字段来自数据库未过滤内容,攻击就成立了。
- 错误做法:在控制器里写
return json(['content' => $content]);,然后前端el.innerHTML = data.content; - 正确做法(后端预处理):
$data['content'] = htmlspecialchars($data['content'], ENT_QUOTES | ENT_HTML5, 'UTF-8');再return json($data); - 更稳妥(尤其富文本):
$data['content'] = strip_tags($data['content'], '<p><br><strong><em>'); $data['content'] = htmlspecialchars($data['content'], ENT_QUOTES | ENT_HTML5, 'UTF-8');</em></strong></p><div class="aritcle_card flexRow artxards"> <div class="artcardd flexRow"> <a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill5233" title="btpanel phpsite 宝塔面板PHP网站"><img src="https://img.php.cn/upload/skill/000/000/081/179040786932301.jpg" alt="btpanel phpsite 宝塔面板PHP网站" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a> <div class="aritcle_card_info flexColumn"> <a rel="nofollow" href="/xiazai/skill5233" title="btpanel phpsite 宝塔面板PHP网站" class="overflowclass">btpanel phpsite 宝塔面板PHP网站</a> <p class="overflowclass">宝塔面板 PHP 网站管理:站点创建、删除、启停、PHP 版本切换、域名管理、SSL证书管理、伪静态管理、数据库管理</p> </div> <a rel="nofollow" href="/xiazai/skill5233" title="btpanel phpsite 宝塔面板PHP网站" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a> </div> </div>
- 绝对不要在前端 JS 里依赖
textContent或简单replace()来“过滤”,用户可绕过前端直调接口
富文本内容(如 TinyMCE 提交)怎么安全输出?
htmlspecialchars() 会把所有标签变成乱码,strip_tags() 或正则 DIY 过滤根本防不住 <img src="x" onerror="alert(1)"> 或 <svg onload="alert(1)"></svg>。必须用白名单机制。
- 安装:
composer require ezyang/htmlpurifier - 关键配置必须显式设置,不能用
HTMLPurifier_Config::createDefault():HTML.Allowed(如'p,b,i,u,a[href|title],img[src|alt|width|height]')、URI.AllowedSchemes(只留['http', 'https', 'mailto'])、Core.RemoveScriptContents(设为true) - 净化动作应放在**模型保存前**或**模板输出前**,而不是控制器里每次 new 实例;封装成静态方法或服务复用
- 数据库存原始 HTML,别提前
htmlspecialchars()存进去——否则后续想导出、API 返回、编辑回填都会出问题
为什么 default_filter 配置靠不住?
它只对 input()、I() 等框架输入函数获取的变量生效,且参数不全、上下文盲、易被绕过。
- 绕过方式极多:直接读
$_POST、从数据库查出后echo、用file_get_contents('php://input')接 JSON body - 默认不传
ENT_QUOTES和'UTF-8',老 PHP 版本下可能被双字节编码绕过 - 对 JSON 输出、JS 内联、URL 属性等场景完全无效:
<a href="<?php%20echo%20%24url;%20?>"></a>里$url含javascript:alert(1),default_filter压根不处理 - 真正要防的不是“怎么拿到数据”,而是“怎么把它安全地放进 HTML/JS/CSS/URL 里”——这个决定权永远在输出点
最危险的疏忽是:以为开了 default_filter 就安全了,或者只在控制器里统一 htmlspecialchars() 所有输入再传给模板。XSS 防御必须是上下文感知的,同一段数据,在 <div> 里、<code>title 属性里、<script></script> 里,要用完全不同的编码方式。别省那几行代码,每个输出点都得亲手确认。










