← 中文资源

简体中文版本

用 DOMPurify 净化用户提交的 HTML

在服务端以严格白名单净化富文本输入,用钩子管住 URL 协议,并用 XSS 载荷测试证明结果。

阅读英文原文

目标

转义 HTML 会毁掉排版,信任 HTML 会毁掉站点。净化是第三条路:解析标记、只保留显式清单内的标签与属性、其余全部丢弃。服务端配严格白名单的 DOMPurify 把任意用户 HTML 变成一小套可预测的词汇表,而 CI 里的载荷语料库让这套词汇表在库与威胁演化中保持诚实。

步骤要点

  1. 用 jsdom + DOMPurify 在服务端净化,ALLOWED_TAGS 只留排版所需、ALLOWED_ATTR 仅 href/title。
  2. 加 uponSanitizeAttribute 钩子:href 不以 http(s): 开头即丢弃,堵住 javascript:/data: 协议。
  3. 在写入边界净化一次并存干净结果,所有读取方与导出方自动继承保证。
  4. 用 img onerror、javascript: 链接、svg onload、iframe srcdoc 等载荷实测输出,确认全部被剥。
  5. 把载荷语料库固化进 CI:任何净化器回归、配置漂移或库降级都会以具名载荷让构建失败。

排错提示

编辑器产出的排版被剥掉说明白名单比编辑器输出窄,应逐标签有意放宽而不是关闭净化,且渲染只用净化后的形态;高并发下净化变慢是因为每个请求新建 JSDOM window,改用池化复用并基准测试;需要 MathML/SVG 时攻击面大增,只允许评审过的子集并专项跑 mXSS 语料。

适用边界

服务端净化保护的是存储与所有客户端,插入端渲染时仍应使用净化结果而不是双重信任;净化器随新绕过演进,库要保持更新、语料库要持续生长。

代码示例

const clean = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "ul", "li", "code", "pre"],
  ALLOWED_ATTR: ["href", "title"],
});

// Hook: href must be http(s)
DOMPurify.addHook("uponSanitizeAttribute", (node, data) => {
  if (data.attrName === "href" && !/^https?:/i.test(data.attrValue)) {
    data.keepAttr = false;
  }
});