thinkphp模型的$filter属性对xss过滤基本没用,因其仅在data()->validate()->save()链式调用且显式启用时触发,而save(['field'=>$raw])等写法直接绕过;真正可靠入口是before_write事件配合htmlpurifier单例处理富文本、htmlspecialchars按字段类型精准转义。

ThinkPHP模型的$fillter属性为什么对XSS过滤基本没用
因为$filter在TP6中仅在data()->validate()->save()链式调用且显式启用时才触发,而save(['field' => $raw])这种写法直接绕过它。你设了protected $filter = ['content' => 'htmlspecialchars'],但只要控制器里写了$model->save(['content' => '<script>alert(1)</script>']),恶意脚本就直入库。
常见错误现象包括:后台编辑器提交的富文本被htmlspecialchars()双转义成源码、toWrite()被误以为能改数据值(其实只映射字段名)、before_write事件没注册或函数签名不对导致不执行。
-
$filter只作用于data()传入的数据,不处理关联保存、Db::table()->insert()、批量更新等场景 - 过滤函数执行时机在验证之后、写入之前,但仅限字段级,无法递归处理JSON字段或嵌套数组
- 若字段本身已含HTML实体(如数据库里存的是
<script></script>),再套一次htmlspecialchars()会变成<script>,前端显示异常
真正生效的XSS过滤入口是before_write事件
这是TP6中唯一稳定、可控、可覆盖所有save()路径的过滤点。必须在模型boot()方法中显式绑定,且sanitizeFields()需手动遍历并区分字段类型——不能无差别调用htmlspecialchars()。
使用场景:纯文本字段(如title、desc)需要转义;富文本字段(如content)必须跳过,否则破坏排版;JSON字段(如config)要先json_decode()再递归处理字符串值,最后json_encode()回写。
- 注册方式:
self::event('before_write', [self::class, 'sanitizeFields']); - 在
sanitizeFields()中用array_walk_recursive()扫描值,对is_string($val) && !preg_match('/^的字段才调用<code>htmlspecialchars($val, ENT_QUOTES | ENT_HTML5, 'UTF-8') - 避免重复转义:检查字符串是否已含
<、>等实体,有则跳过
富文本字段必须用HTMLPurifier白名单清洗,不能靠htmlspecialchars
htmlspecialchars()会让<p>Hello</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill6712" title="Wechat HTML Publisher"><img
src="https://img.php.cn/upload/skill/000/000/081/179109368394970.jpg" alt="Wechat HTML Publisher" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill6712" title="Wechat HTML Publisher" class="overflowclass">Wechat HTML Publisher</a>
<p class="overflowclass">直接上传HTML富文本到微信公众号草稿箱。支持完整的HTML格式,无需Markdown转换。</p>
</div>
<a rel="nofollow" href="/xiazai/skill6712" title="Wechat HTML Publisher" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>变成纯文本,而用户真正需要的是渲染后的段落。XSS防护的关键不是“不让HTML出现”,而是“只允许安全的HTML”。这时候HTMLPurifier是唯一靠谱的选择。
注意:不要在app/common.php里new HTMLPurifier(),这会导致每次调用都重新编译规则,性能暴跌。必须通过容器单例注册。
- 注册单例位置:
app\common\provider\AppServiceProvider.php中的register()方法 - 绑定方式:
app()->bind('HtmlPurifier', function () { return new \HTMLPurifier(\HTMLPurifier_Config::createDefault()); }); - 白名单配置示例:
$config->set('HTML.Allowed', 'p,b,i,a[href|title],img[src|alt|width|height]');,禁用on*事件和javascript:协议 - 调用时机:在
before_write中识别出富文本字段后,用app('HtmlPurifier')->purify($value)处理
别在中间件或控制器里提前过滤input()数据
有人习惯在中间件里对$_POST全量调用htmlspecialchars(),结果导致API返回的JSON里全是<,前端解析失败;或者控制器里$data = input(); array_walk($data, 'htmlspecialchars');,后续想把$data['title']塞进<meta name="description" content="...">时,双引号没被ENT_QUOTES保护,被空格截断后注入onclick。
真正该过滤的地方只有两个:入库前(按字段类型选htmlspecialchars或HTMLPurifier)、输出时(按上下文选htmlspecialchars、json_encode或HTMLPurifier)。中间件只适合处理明确用于HTML展示的键(如['title', 'nickname']),且不能修改$request对象本身。
- 禁止在中间件里遍历
$_REQUEST做全局过滤,TP6的Request是只读快照,强行改会引发不可预测行为 -
input('name', '', 'htmlspecialchars')仅限纯文本展示场景,且第三参数必须传全:input('name', '', 'htmlspecialchars,ENT_QUOTES,UTF-8') - 模板里
{$content|htmlspecialchars}默认不带ENT_QUOTES,等价于htmlspecialchars($content, ENT_COMPAT),单引号漏转——务必用{:htmlspecialchars($content, ENT_QUOTES | ENT_HTML5, 'UTF-8')}
最易被忽略的点是:富文本字段入库前必须走HTMLPurifier,但它的配置必须单例化;before_write事件里处理JSON字段时,要先解码再递归过滤字符串,否则array_walk_recursive()对JSON字符串无效;所有htmlspecialchars()调用必须显式传ENT_QUOTES和'UTF-8',缺一不可。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










