外部非受控代码注入需安全使用而非禁用,DOMPurify因基于HTML解析器重建DOM并白名单裁剪,可防嵌套绕过、编码混淆等攻击;sanitize-html适用于服务端预处理,DOMPurify用于客户端渲染前净化;textContent和createElement可规避风险但不支持富文本;CSP是必要兜底措施,须正确配置且禁用unsafe-inline。

外部非受控代码注入(如用户提交的富文本、第三方 API 返回的 HTML 片段、CMS 动态内容)不是“能不能用”的问题,而是“怎么安全用”的问题。直接 innerHTML 赋值或模板字符串拼接等于主动交出页面控制权——浏览器会照单解析执行,<script>、onerror、javascript: 都会立刻触发。
为什么 DOMPurify 是当前最可靠的选择
它不是简单删标签,而是基于 HTML 解析器重建 DOM 树,再按白名单策略逐节点裁剪。相比正则过滤或黑名单式清理,它能防住嵌套绕过(如 <img src=1 onerror="alert(1)">)、编码混淆(如 <script></script>)、属性污染(如 style="background:url(javascript:alert())")等常见 bypass 手段。
- 必须用最新版(v3.0+),旧版本对 SVG 和 MathML 的处理存在已知绕过
- 默认配置不启用
SAFE_FOR_TEMPLATES,若用于 Vue/React 的插值上下文,需显式开启 - 对
data-属性不做自动清理,如有业务依赖自定义data-*,需在ALLOWED_DATA_ATTR中声明 - 不处理 CSS 注入,若允许
style属性,需额外用CSS.escape()或库(如css-sanitize)二次过滤
富文本场景下,sanitize-html 与 DOMPurify 怎么选
二者定位不同:sanitize-html 是服务端 Node.js 库,适合预处理存储;DOMPurify 是客户端运行时净化,适合渲染前最后一道防线。不能混用,也不能只用其一。
- 服务端入库前必须用
sanitize-html过滤:配置allowedTags(如['p', 'br', 'strong', 'a'])、allowedAttributes(如{ a: ['href'] })、强制allowedSchemes为['http', 'https', '/'],拒绝javascript:和data: - 客户端渲染时仍需
DOMPurify.sanitize():因为服务端净化结果可能被中间代理篡改,或前端 JS 逻辑意外拼接了新内容 - 若使用 SSR(如 Next.js),服务端和客户端净化配置必须严格一致,否则出现“服务端渲染正常、客户端 hydration 失败”
textContent 和 createElement 不能替代净化,但能规避大部分风险
当内容纯文本、或结构固定且可控时,这是比任何净化库都更轻量、更确定的安全方案。但它不解决“需要保留部分格式”的真实业务需求(比如论坛发帖、后台编辑器输出)。
立即学习“前端免费学习笔记(深入)”;
-
textContent自动转义所有 HTML 字符,<script>alert(1)</script>会原样显示为文字 -
createElement+appendChild构建节点时,传入的字符串仍会被当作文本处理,不会触发解析 - 错误用法:
el.innerHTML = '<span class="user">' + userInput + '</span>'—— 即使只包一层 span,userInput里的</span>也会提前闭合标签,导致后续内容逃逸 - 正确写法:
const span = document.createElement('span'); span.className = 'user'; span.textContent = userInput; el.appendChild(span);
CSP 不是可选项,而是兜底必须项
即使净化逻辑出错、或某处漏掉 DOMPurify 调用,CSP 也能阻止脚本执行、限制资源加载范围。但它的作用前提是配置正确,且不带 'unsafe-inline'。
- 必须设置
script-src 'self' 'nonce-<随机值>',并在每个内联<script>标签加nonce属性 - 对富文本中允许的
<img>、<a>,需同步配置img-src和connect-src,否则图片加载失败或 AJAX 被拦截 - 开发期用
Content-Security-Policy-Report-Only收集违规日志,上线前确认无误再切为正式策略 - 注意:CSP 对
javascript:伪协议无效,必须靠净化层提前剔除
真正难的不是选哪个库,而是把净化动作嵌入到每一条数据流路径里——API 响应、本地缓存、SSR 渲染、客户端 patch、甚至 localStorage 读写。漏掉任意一环,就等于在防线上开了个口子。



















