必须用HTMLPurifier净化富文本,仅前端处理或其它方案不可靠;htmlspecialchars仅适用于纯文本上下文,误用会导致XSS;入库前净化一次即可,渲染前勿重复;contenteditable内容须用编辑器API获取。

富文本输出前必须走 HTMLPurifier,别信“前端已处理”
后端不净化 = 直接放行 XSS。DOMPurify、v-html 预处理、编辑器粘贴过滤,全可被绕过——攻击者 curl 一发 POST 就进库了。HTMLPurifier 是 PHP 生态唯一经长期实战检验的方案,但默认配置几乎禁全部标签,必须显式声明白名单。
常见错误:HTMLPurifier_Config::createDefault() 开箱即用 → 放行危险属性;只配 HTML.Allowed 却漏掉 URI.AllowedSchemes → href="javascript:alert(1)" 照样存活。
-
HTML.Allowed示例值:'a[href|title],strong,em,p,br,ul,ol,li,img[src|alt|width|height]' -
URI.AllowedSchemes必须显式设为['http', 'https', 'mailto'],禁用data:和javascript: - 图片内联样式被清空?不是 bug:要保留
background-size,得开CSS.AllowTricky => true并手动加进CSS.AllowedProperties
htmlspecialchars() 只能用于纯文本上下文,误用等于埋雷
htmlspecialchars($input) 没传参数是高危操作:单引号不转义,' 可绕过;不指定编码时,GBK 双字节字符可能触发截断+二次解码漏洞。它只适用于插入 <p></p> 内容、title 属性、<textarea value></textarea> 这类纯文本位置。
典型误用:
立即学习“前端免费学习笔记(深入)”;
- 把字段塞进 JSON 响应前调
htmlspecialchars()→ 应该用json_encode($str, JSON_UNESCAPED_UNICODE) - 对已用
htmlspecialchars()处理过的字符串再喂一次 →&变成&,显示异常 - 在
href或onclick中直接插变量 → 必须换上下文专用防护,比如filter_var($url, FILTER_VALIDATE_URL)+ 白名单协议校验
富文本入库前别预净化,渲染前也别重复净化
存库时做一次 HTMLPurifier 净化,渲染时再跑一遍,看似更安全,实则引入两重风险:性能开销翻倍(尤其高频请求),且可能误杀合法实体(如 © 被转成 © 后再转义成 ©)。
真正该做的:
- 入库前净化一次,确保存储的是干净 HTML
- 渲染前不再调用 HTMLPurifier,除非你明确知道数据来源不可信(比如从第三方 API 拉取)
- 若用模板引擎,确认
{{ }}默认转义是否开启;遇到{{ raw }}或 Vue 的v-html,必须确保变量已由服务端净化过
contenteditable 区域获取 HTML 不能直接读 innerHTML
直接读 el.innerHTML 会暴露浏览器自动补全的标签(如把孤立 <li> 套进 <ul></ul>)、残留编辑器私有属性(data-mce-* 、class="Apple-style-span")、甚至零宽空格(ZWSP)导致后端解析失败。
安全做法:
- 优先用编辑器 API:Quill 的
getSemanticHTML()、CKEditor5 的editor.data.get({ trim: 'both' }) - 必须手撸时,先
cloneNode(true),再遍历移除非白名单属性(class、style、data-*等),最后取clone.innerHTML - 输出前仍需过一遍
DOMPurify.sanitize(html, { ALLOWED_TAGS: [] })防兜底,别跳过
复杂点在于:不同上下文(属性 vs 文本 vs JS 字符串)需要完全不同的防护手段,而多数人只记得“转义”二字。一个 href 属性里的 URL,和一段插入 <div> 的富文本,根本不能共用同一套函数。</div>



















