用 DOMPurify 构建可靠的 XSS 防御层

为什么 XSS 依然是前端安全的头号威胁

跨站脚本攻击(XSS)长期占据 OWASP Top 10 的显眼位置,原因很简单:只要页面会渲染用户提供的内容,就存在注入风险。评论区、富文本编辑器、Markdown 渲染、URL 参数回显——这些常见功能都是 XSS 的温床。

XSS 的常见方式主要有三类:

存储型 XSS 是最危险的一种。攻击者将恶意脚本提交到服务器(比如一条评论),服务器将其存入数据库,之后每个访问该页面的用户都会执行这段脚本。危害范围大、持续时间长。

反射型 XSS 通常通过精心构造的 URL 诱导用户点击,恶意脚本作为请求参数发送到服务器,服务器未经处理直接回显在页面中。常见于搜索框、错误提示页。

DOM 型 XSS 完全发生在浏览器端。前端 JavaScript 从 location.hashdocument.referrer 等来源读取数据,并通过 innerHTMLdocument.write 等方式插入页面,整个过程不经过服务器。

传统的防御思路是“转义一切”,但现实远比这复杂。现代 Web 应用需要允许一部分 HTML 标签和属性通过(比如加粗、链接、图片),纯粹的转义会让富文本功能彻底失效。手工编写白名单过滤器又极易遗漏边缘情况——浏览器对畸形 HTML 的容错解析能力,远超大多数开发者的想象。

DOMPurify 的定位与核心优势

DOMPurify 是一个专注于 HTML、SVG 和 MathML 净化的 JavaScript 库。它的设计哲学非常明确:只做一件事,并且做到极致

它的核心优势体现在几个方面:

  • 基于浏览器原生解析器:DOMPurify 利用浏览器自身的 DOM 解析引擎来构建节点树,这意味着它能正确处理浏览器实际会执行的畸形 HTML,而不是按照规范文档去猜测。
  • 默认安全:开箱即用的配置已经能抵御绝大多数 XSS 向量,包括 javascript: 协议、onerror 等事件属性、<script> 标签、SVG 中的脚本等。
  • 可配置的白名单机制:允许开发者精确控制哪些标签、属性、CSS 属性可以通过。
  • 持续维护与广泛验证:被 Google、Microsoft、Mozilla 等团队使用,安全社区持续对其进行审计和绕过测试。

基础用法:从危险到安全

安装方式很简单:

npm install dompurify

最基本的净化操作:

import DOMPurify from 'dompurify';

const dirty = '<img src=x onerror=alert(1)>';
const clean = DOMPurify.sanitize(dirty);
// 输出: <img src="x">

onerror 属性被移除,alert(1) 不会执行。这就是 DOMPurify 的默认行为——移除所有可执行脚本和事件处理器。

对于需要渲染用户提交的 HTML 内容的场景:

const userContent = '<p>Hello <strong>World</strong></p><script>stealCookies()</script>';
const safeContent = DOMPurify.sanitize(userContent);
document.getElementById('output').innerHTML = safeContent;

<script> 标签被剥离,安全的标签和文本保留。

进阶配置:在安全与功能之间取得平衡

实际项目中,我们往往需要允许特定标签。比如一个博客系统需要支持图片、链接、代码块:

const config = {
  ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a', 'img', 'code', 'pre', 'blockquote', 'ul', 'ol', 'li'],
  ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class'],
  ALLOW_DATA_ATTR: false,
  ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i
};

const clean = DOMPurify.sanitize(userHtml, config);

这里有几个关键点:

  • ALLOWED_URI_REGEXP 限制了链接和图片地址的协议,阻止 javascript:data: 等危险协议。
  • ALLOW_DATA_ATTR: false 禁止 data-* 属性,防止某些框架通过 data 属性触发行为。
  • 显式列出允许的标签和属性,而不是依赖默认值。

如果需要更严格的控制,还可以使用钩子函数:

DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
  if (data.attrName === 'href' && !data.attrValue.startsWith('https://')) {
    data.attrValue = '';
  }
});

与其他防御手段的配合

DOMPurify 是前端 XSS 防御的重要一环,但不是全部。完整的防御体系需要前后端协同:

前端层面,除了 DOMPurify,还应避免使用 eval()new Function()setTimeout(string) 等动态执行代码的方式。对于纯文本展示场景,优先使用 textContent 而非 innerHTML

后端层面,MySQL 防止 SQL 注入的几种写法同样值得重视。最可靠的方式是使用参数化查询(预处理语句),例如在 Node.js 中:

// 正确:参数化查询
db.query('SELECT * FROM users WHERE email = ?', [email]);

// 错误:字符串拼接
db.query(`SELECT * FROM users WHERE email = '${email}'`);

在 PHP 中使用 PDO 的预处理语句,在 Java 中使用 PreparedStatement,原理相同。此外,对输入进行类型验证、最小化数据库账户权限、使用 ORM 框架的查询构造器,都是有效的辅助手段。

服务器层面,一份 Linux 服务器安全加固清单可以帮助缩小攻击面:及时更新系统和软件包、禁用不必要的服务、配置防火墙规则、使用 SSH 密钥认证并禁用密码登录、设置 fail2ban 防止暴力破解、定期审计日志和文件权限。

常见误区与注意事项

不要直接拼接后再净化。有些开发者会先把用户输入拼接到模板中,再对整体调用 sanitize。这可能导致净化器无法正确识别某些上下文。正确的做法是先净化用户输入,再插入到模板中。

服务端渲染场景。在 Node.js 中使用 DOMPurify 需要配合 jsdom,因为 DOMPurify 依赖 DOM 环境。配置如下:

import { JSDOM } from 'jsdom';
import createDOMPurify from 'dompurify';

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

保持更新。XSS 绕过技术不断演进,DOMPurify 团队会持续发布安全补丁。将 DOMPurify 锁定在过旧版本,可能错过关键修复。

不要过度依赖单一防线。即使使用了 DOMPurify,也应该配置 Content-Security-Policy(CSP)响应头,限制脚本来源和 unsafe-inline 的使用。纵深防御永远优于单点依赖。

结语

XSS 防御没有银弹,但 DOMPurify 提供了一层经过实战检验的可靠屏障。它的价值在于将复杂的 HTML 净化逻辑封装成简单、安全的 API,让开发者不必成为浏览器解析器专家也能有效防御 XSS。结合后端参数化查询、服务器加固和 CSP 策略,可以构建起从浏览器到数据库的完整防御链。安全是一个持续的过程,选择正确的工具并保持警惕,才是最务实的策略。

未经允许不得转载:任鹏个人博客 » 用 DOMPurify 构建可靠的 XSS 防御层

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏