富文本输出的HTML不能直接innerHTML渲染,因其含冗余标签、残留属性(如data-mce-*)、非法嵌套(<p><div>文本</div></p>)、不可见字符(如ZWSP)及XSS风险点(onerror、javascript:等),导致解析异常、布局错乱、CSS失效;必须先DOM克隆+白名单过滤,再经DOMPurify二次净化,并嵌入数据流各环节持续清洗。

富文本输出的 HTML 为什么不能直接 innerHTML 渲染
因为编辑器输出的 HTML 天然含冗余标签、残留属性(data-mce-*、class="Apple-style-span")、非法嵌套(<p><div>文本</div></p>)和不可见字符(如 ZWSP),浏览器解析时会自动补全或报错,导致布局错乱、CSS 失效、移动端换行异常。更严重的是,它可能包含未被编辑器过滤干净的 onerror、javascript: 或内联 style 注入点。
用 DOMPurify 做清洗前必须先克隆节点
直接对 editor.root.innerHTML 或 contenteditable 元素调用 DOMPurify.sanitize() 是无效的——DOMPurify 输入的是字符串,而原始 HTML 字符串里已混入浏览器自动修正的结构、转义失败的字符、甚至注释中隐藏的恶意片段。正确做法是:
- 先
const clone = el.cloneNode(true)获取干净 DOM 树 - 遍历移除所有非白名单属性:
class、style、id、data-* - 只保留语义化标签:
p、h1–h6、ul/ol/li、strong、em、a、img - 再把
clone.innerHTML传给DOMPurify.sanitize(),并显式配置ALLOWED_TAGS和ALLOWED_ATTR
Quill/TinyMCE/CKEditor5 输出 HTML 的关键清洗时机
不同编辑器暴露内容的方式差异很大,清洗不能“一刀切”:
-
Quill:别用root.innerHTML,优先调用editor.getSemanticHTML()(需插件)或editor.clipboard.convert();若必须导出 HTML,确保已禁用clipboard.matchers中的危险转换逻辑 -
TinyMCE:启用valid_elements: "p,strong,em,a[href],img[src|alt]"+forced_root_block: "p",避免输出div段落 -
CKEditor5:禁用htmlSupport插件,改用editor.data.get({ trim: 'both' });若必须支持自定义 HTML,需配套编写完整的upcast/downcast规则,否则仍会被丢弃
React 中渲染富文本 HTML 的唯一安全路径
dangerouslySetInnerHTML 不是“危险但可用”,而是“仅当内容已通过服务端+前端双重净化后才可触达”。实际操作中必须满足:
立即学习“前端免费学习笔记(深入)”;
- 前端清洗后,仍需服务端用
Cheerio或jsdom二次解析:检查是否含<html>/<head>、是否有孤立<img>、是否所有纯文本都被<p>包裹 - 禁止在组件内做任何“动态拼接 HTML 字符串”的行为,包括
innerHTML += ...或document.write() - 若内容来自用户提交,且无法保证清洗完整性,降级为
textContent渲染,或强制转 Markdown 再用安全解析器(如remark)转 HTML
最易被忽略的一点:清洗不是一次性的动作。用户粘贴、编辑器自动补全、服务端回填、前端 diff 更新,每个环节都可能引入新污染。清洗逻辑必须嵌入到数据流入口,而非仅放在最终渲染前。



















