dompurify 不是编辑器插件,必须在提交/渲染/入库前显式调用 sanitize() 处理原始 html 字符串;需严格配置 allowed_tags、allowed_attr 白名单,服务端必须二次净化,且 node.js 环境需用 jsdom 模拟 dom。

DOMPurify 不能“集成到编辑器 UI”里运行
它不是富文本编辑器的插件,也不支持挂载在 TinyMCE、Quill 或 CKEditor 的界面上实时监听 DOM 变化。所有“编辑器内自动拦截 XSS”的想法,都混淆了输入来源和净化时机——用户在编辑器里敲出的 <img src="x" onerror="alert(1)"> 是未信任内容,必须在提交前、渲染前或服务端入库前,用 DOMPurify.sanitize() 处理原始字符串。
常见错误现象:直接把 editor.getContent() 结果赋给 innerHTML,XSS 立刻触发;在 onChange 里反复调用 sanitize(),导致光标跳动、样式丢失、性能卡顿,且对嵌套 SVG 或注释包裹的 payload 无效。
- 正确路径是:获取原始 HTML 字符串 →
DOMPurify.sanitize(dirtyHtml, config)→ 得到 cleanHtml → 再用于渲染或提交 - Quill 示例:
quill.root.innerHTML;TinyMCE 示例:tinymce.activeEditor.getContent() - 别信“编辑器自己处理了”——Trix 虽内置
HTMLSanitizer模型,但仍是调用DOMPurify.sanitize(),不是魔法
生产环境必须显式配置 ALLOWED_TAGS 和 ALLOWED_ATTR
默认配置允许约 30 个基础标签(p、br、strong 等),但会保留 style 和 class 属性——这等于给 CSS 注入留了后门,比如 <div style="background:url(javascript:alert())"> 就能绕过。
<p>真正起效的是白名单机制:没列进 <code>ALLOWED_ATTR 的属性一律被删,包括 style、class、data- 自定义属性(除非你明确加进去)。
-
ALLOWED_TAGS:只放你真需要的,例如['p','br','strong','em','a','ul','ol','li','code'] -
ALLOWED_ATTR:严格控制,['href','src','alt','title','width','height']已覆盖多数需求;style和class别加,除非你有完整白名单策略(如 Tailwind 的 class 前缀限制) -
FORBID_TAGS和FORBID_ATTR是冗余保险,不替代白名单;漏配ALLOWED_URI_REGEXP会导致javascript:、data:协议逃逸
服务端调用 DOMPurify 必须配 jsdom,否则报 window is not defined
Node.js 环境没有 window,直接 require('dompurify') 并调用 sanitize() 会抛 ReferenceError: window is not defined。必须用 jsdom 模拟 DOM 环境。
关键点:创建 JSDOM 实例时,runScripts 必须设为 false('dangerously' 仅限测试,生产严禁);再用 createDOMPurify(dom.window) 初始化库。
- 安装:
npm install dompurify jsdom - 配置建议加
RETURN_TRUSTED_TYPE: true,配合浏览器 Trusted Types API 提升纵深防御 - 前端净化可被 curl、Postman、禁用 JS 完全绕过,服务端二次净化不是可选项,是强制项
v-html 和 dangerouslySetInnerHTML 渲染前必须过一遍 sanitize()
Vue 的 v-html 和 React 的 dangerouslySetInnerHTML 不做任何过滤,只是把字符串丢给 innerHTML。哪怕你刚从 Quill 拿到内容,也得立刻调用 DOMPurify.sanitize()。
容易被忽略的场景:后端返回的评论列表 HTML、CMS 导入的旧内容、第三方 API 同步的富文本——这些都不是“用户当前提交”,但同样可能被污染,渲染前仍需净化。
- 不要在
input或paste事件里实时 sanitize:拦不住拖放、语音输入、剪贴板粘贴等非键盘输入 - 正确时机是表单提交前,或后端响应解析后、渲染前
- 如果保留
target="_blank",需用 hook 补rel="noopener":DOMPurify.addHook("afterSanitizeAttributes", node => { if (node.tagName === "A" && node.hasAttribute("target")) node.setAttribute("rel", "noopener"); })
DOMPurify.sanitize(),就等于没防住 XSS。最常被跳过的环节,恰恰是服务端入库前那一次调用。











