富文本过滤必须用HTMLPurifier等白名单解析器做后端净化,因htmlspecialchars()会破坏标签语义、strip_tags()无法清除危险属性,二者均不解析DOM且易被绕过。

富文本过滤不能靠前端拦、不能靠正则删、不能靠strip_tags()糊弄——后端必须用白名单解析器做最终净化,否则 XSS 就在下一次渲染时触发。
为什么htmlspecialchars()和strip_tags()对富文本完全无效
这两个函数根本不是为富文本设计的:htmlspecialchars()会把所有 HTML 标签转成纯文本(比如 <p>Hello</p>),用户发的加粗、链接全消失;strip_tags()只删标签外壳,<p onclick="alert(1)">里的 onclick 属性原封不动保留,照样执行。
- 它不解析 DOM 结构,无法识别
javascript:、onerror、style="x:expression()"这类危险模式 - 它不校验属性值,
href="javascript:alert(1)"也能绕过 - 它不处理嵌套注释、CDATA、畸形闭合等浏览器实际会解析的边缘 case
PHP 后端必须用 HTMLPurifier 或 XssHtml 做入库前净化
这是目前 PHP 生态唯一经长期实战验证的方案。关键不是“用了就行”,而是配置必须显式、收紧:
-
HTML.Allowed必须写全,例如'a[href|title],strong,em,p,br,ul,ol,li,img[src|alt|width|height]',不能留空或用默认值 -
URI.AllowedSchemes必须限定为['http', 'https', 'mailto'],否则data:和javascript:仍可注入 - 禁用宽松模式:
HTMLPurifier_Config::createDefault()默认太松,要手动覆盖 - 别在存储时净化一次、输出时再净化一次——重复处理可能把
©变成©,显示异常
前端 DOMPurify 只能当“防手滑”用,不能替代后端
用户禁用 JS、用 Postman 直 POST、甚至 curl 提交,DOMPurify.sanitize() 根本没机会运行。它只适合做预览层的体验优化:
立即学习“前端免费学习笔记(深入)”;
- 表单提交前对
textarea.value跑一次轻量DOMPurify.sanitize(),仅用于页面内实时预览 - 但原始未净化内容必须原样发给后端,由后端决定最终存什么
- Vue 中禁用
v-html绑定未经后端净化的变量;React 中绝不用dangerouslySetInnerHTML接原始富文本 - 校验
a[href]不能只看标签,得调DOMPurify.isValidAttribute('a', 'href', value)单独过一遍
Java 项目推荐用 jsoup 的 Safelist
比正则可靠,比自研解析器省心,且支持细粒度控制:
- maven 引入
org.jsoup:jsoup:1.14.3或更高稳定版 - 用
Safelist.relaxed().addTags("img").addAttributes("img", "src", "alt")构建白名单,别用basic()或none() - 对
src属性额外加协议校验:Whitelist.relaxed().addProtocols("img", "src", "http", "https") - 注意:它不自动修复编码问题,若输入含 GBK 双字节截断字符,需先统一转 UTF-8 再进过滤流程
最常被忽略的一点:富文本字段的「入库」和「输出」是两个独立环节,净化动作只能落在入库前或首次渲染前——中间任何缓存、CDN、日志打印环节若直接吐出原始内容,就等于把漏洞暴露在链路里。安全不是加一层函数,而是守住那个唯一可信的净化点。



















