目标
转义 HTML 会毁掉排版,信任 HTML 会毁掉站点。净化是第三条路:解析标记、只保留显式清单内的标签与属性、其余全部丢弃。服务端配严格白名单的 DOMPurify 把任意用户 HTML 变成一小套可预测的词汇表,而 CI 里的载荷语料库让这套词汇表在库与威胁演化中保持诚实。
步骤要点
- 用 jsdom + DOMPurify 在服务端净化,ALLOWED_TAGS 只留排版所需、ALLOWED_ATTR 仅 href/title。
- 加 uponSanitizeAttribute 钩子:href 不以 http(s): 开头即丢弃,堵住 javascript:/data: 协议。
- 在写入边界净化一次并存干净结果,所有读取方与导出方自动继承保证。
- 用 img onerror、javascript: 链接、svg onload、iframe srcdoc 等载荷实测输出,确认全部被剥。
- 把载荷语料库固化进 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;
}
});