1. 项目概述:为什么Vue3项目必须关注XSS防护

在Vue3项目中处理用户输入时,一个看似简单的需求——过滤特殊字符,背后往往关联着Web安全中最常见也最危险的漏洞之一:跨站脚本攻击。很多开发者,尤其是刚接触Vue3生态的朋友,可能会觉得框架本身已经提供了 v-html 的警告,或者认为现代前端框架的响应式系统能自动规避这些问题。但实际情况是,XSS的防御是一个多层次、需要主动干预的工程。无论是用户评论、富文本编辑器内容、还是从第三方接口获取并需要动态渲染的数据,只要存在将字符串当作HTML解析的机会,攻击者就可能注入恶意脚本,窃取用户Cookie、发起非法请求,甚至控制用户会话。

我接手过不少从Vue2迁移到Vue3的项目,发现一个普遍现象:大家热衷于使用 <script setup> 、组合式API、Vite这些新特性提升开发体验,但在安全防护的实践上,往往还停留在“用正则表达式过滤一下”的初级阶段。正则表达式对付简单的 <script> 标签或许有效,但面对HTML实体编码、事件处理器属性、 javascript: 伪协议、CSS表达式等五花八门的攻击向量,就显得力不从心,极易产生遗漏。

这就是为什么我们需要一个专门、健壮的解决方案。 DOMPurify 正是为此而生。它是一个仅针对DOM的、超快速、超宽容的XSS净化工具。它的核心逻辑不是用黑名单去猜测哪些是“坏”的,而是采用白名单机制,只允许已知安全的HTML元素和属性通过,其他一律清除或转义。在Vue3的上下文中集成 DOMPurify ,意味着我们可以在数据流入视图层之前,筑起一道可靠的防线,确保动态内容的渲染安全无虞。这不仅是功能实现,更是项目上线前必须通过的安全审计项。

2. 核心思路与方案选型:为什么是DOMPurify?

面对XSS防护,开发者通常有几个选择:手动转义、使用内置API、引入专用库。我们需要逐一分析,才能理解为什么 DOMPurify 在Vue3场景下是最佳实践。

2.1 常见方案对比与陷阱

  • 方案一:手动转义或简单正则过滤 这是最原始的方法。例如,写一个函数将 < > & " ' 等字符替换为对应的HTML实体( &lt; &gt; 等)。或者写一个正则表达式去移除 <script> 标签。

    • 为什么不行? XSS的攻击面极其广泛。除了 <script>alert(1)</script> 这种明显的形式,还有:
      • 事件处理器 <img src=x onerror=alert(1)>
      • HTML属性 <a href="javascript:alert(1)">点击</a>
      • CSS注入 <div style="background:url(javascript:alert(1))">
      • SVG/数学ML :这些标记语言内也可能包含可执行脚本。
      • 编码绕过 :攻击者可能使用十进制、十六进制HTML实体或Unicode来混淆过滤逻辑。 手动实现一个能覆盖所有情况的过滤器,复杂度极高,且极易因考虑不周而产生漏洞。安全领域有句老话:“不要自己发明加密算法”,同样,也不要自己发明XSS过滤器。
  • 方案二:依赖Vue的文本插值与 v-html Vue的模板语法( {{ }} )会自动对数据进行HTML转义,这是安全的。问题出在 v-html 指令上,它是Vue提供的、用于输出原始HTML的“逃生舱”。Vue会在控制台给出警告:“注意:在网站上动态渲染任意HTML非常危险,因为它很容易导致XSS攻击。仅在可信内容上使用 v-html ,永远不要用于用户提交的内容。”

    • 关键点 :Vue只负责警告,不负责净化。它把安全的责任完全交给了开发者。如果你确信内容安全(比如来自完全受控的后端,且已净化),可以使用 v-html 。但对于任何来自用户或不可信源的内容,直接使用 v-html 等于开门揖盗。
  • 方案三:使用浏览器内置的 textContent 或创建文本节点 如果我们只是想安全地显示一段文本,完全避免HTML解析,那么 textContent 属性是完美的。它不会将字符串当作HTML解析,而是原样输出。但这无法满足“需要渲染部分安全HTML”的需求,比如用户评论里包含加粗、斜体、链接等合法格式。

2.2 DOMPurify的优势解析

经过对比, DOMPurify 的优势就非常明显了:

  1. 白名单机制 :这是其安全性的基石。它维护了一个庞大的、经过安全评估的“允许名单”,包括安全的标签(如 <b> , <i> , <a> , <span> )、安全的属性(如 href , title , class ,且会对 href 的值进行协议检查,禁止 javascript: )。不在名单上的东西默认会被丢弃。这种“默认拒绝”的策略比“默认允许”要安全得多。
  2. 配置灵活 :你可以通过配置对象,轻松地扩展或缩减这个白名单。例如,你的应用只需要 <b> <i> 标签,你可以将其他所有标签禁用;或者你需要支持 <iframe> ,但必须限制其 src 为特定的域名。
  3. 处理复杂 :它能智能处理嵌套的恶意代码、多种编码方式的攻击载荷、以及各种边缘情况,其测试用例覆盖了成千上万种已知的XSS攻击向量。
  4. 与DOM协同 :它直接在DOM环境下工作,解析、净化、返回一个安全的HTML字符串或DOM节点,与Vue的 v-html 指令可以无缝衔接。
  5. 轻量且高效 :库的体积小,净化速度快,对应用性能影响微乎其微。

因此,在Vue3项目中,对于需要渲染富文本或不可信HTML的场景,标准做法是: 使用 DOMPurify 对原始字符串进行净化,然后将净化后的安全字符串通过 v-html 指令进行渲染。 这样既满足了功能需求,又恪守了安全底线。

3. 在Vue3项目中集成与配置DOMPurify

理论清晰了,接下来我们一步步在Vue3项目中落地。我将以最常见的Vite + Vue3 + TypeScript项目为例进行说明。

3.1 安装依赖

首先,通过npm或yarn安装 DOMPurify 及其对应的TypeScript类型定义文件。

npm install dompurify
npm install -D @types/dompurify
# 或
yarn add dompurify
yarn add -D @types/dompurify

3.2 创建净化工具函数/Composable

为了在项目中复用,我们通常会创建一个工具函数或一个Vue3组合式函数。我更喜欢将其封装为 composable ,因为它更符合Vue3的组合式逻辑,并且可以方便地与其他组合式函数(如获取数据的逻辑)结合。

src/composables 目录下(如果没有请创建),新建一个文件 useDomPurify.ts

// src/composables/useDomPurify.ts
import DOMPurify from 'dompurify';
import { Ref, ref, watch } from 'vue';

// 定义配置类型,这里只列举常用项,可根据DOMPurify文档扩展
export interface PurifyConfig {
  ALLOWED_TAGS?: string[];
  ALLOWED_ATTR?: string[];
  FORBID_ATTR?: string[];
  ALLOW_DATA_ATTR?: boolean;
  // 更多配置见 https://github.com/cure53/DOMPurify
}

/**
 * 创建一个用于净化HTML的Vue3组合式函数
 * @param initialConfig DOMPurify的初始配置
 * @returns 包含净化函数和动态配置引用的对象
 */
export function useDomPurify(initialConfig: PurifyConfig = {}) {
  // 使用ref来管理配置,使其具有响应性(如果需要动态修改)
  const config = ref<PurifyConfig>({
    // 默认配置:允许一些基本的、安全的标签和属性
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li', 'span', 'div'],
    ALLOWED_ATTR: ['href', 'title', 'target', 'class', 'style'],
    // 禁止一些风险较高的属性,如onerror, onclick等
    FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'],
    // 默认不允许自定义data-*属性,除非明确需要
    ALLOW_DATA_ATTR: false,
    ...initialConfig, // 用户传入的配置可以覆盖默认值
  });

  /**
   * 核心净化函数
   * @param dirty 待净化的原始HTML字符串
   * @param customConfig 可选的本次净化专用配置,会与默认配置合并
   * @returns 净化后的安全HTML字符串
   */
  const sanitize = (dirty: string, customConfig?: PurifyConfig): string => {
    if (!dirty) return '';
    // 合并配置:本次自定义配置 > 实例默认配置
    const finalConfig = { ...config.value, ...customConfig };
    try {
      // 调用DOMPurify.sanitize方法
      return DOMPurify.sanitize(dirty, finalConfig);
    } catch (error) {
      console.error('DOMPurify sanitization error:', error);
      // 净化出错时,返回空字符串是最安全的选择,也可以选择转义后返回
      return '';
    }
  };

  /**
   * 创建一个响应式的净化结果
   * 适用于需要监听源字符串变化并自动净化的场景
   * @param source 一个响应式引用(Ref),包含待净化的字符串
   * @param customConfig 净化配置
   * @returns 一个响应式引用,其值为净化后的安全字符串
   */
  const useSanitized = (source: Ref<string>, customConfig?: PurifyConfig) => {
    const sanitized = ref('');
    watch(
      source,
      (newVal) => {
        sanitized.value = sanitize(newVal, customConfig);
      },
      { immediate: true } // 立即执行一次
    );
    return sanitized;
  };

  return {
    sanitize,
    useSanitized,
    config, // 暴露配置引用,允许组件动态修改(谨慎使用)
  };
}

3.3 在组件中使用

现在,我们可以在任何Vue组件中引入并使用这个 composable 了。

  • 场景一:直接净化并渲染
<!-- src/components/CommentItem.vue -->
<template>
  <div class="comment">
    <!-- 使用v-html渲染净化后的内容 -->
    <div class="content" v-html="safeContent"></div>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { useDomPurify } from '@/composables/useDomPurify';

const props = defineProps<{
  rawContent: string; // 从API获取的原始评论内容
}>();

const { sanitize } = useDomPurify();

// 使用计算属性,当rawContent变化时,自动重新净化
const safeContent = computed(() => sanitize(props.rawContent));
</script>
  • 场景二:处理富文本编辑器内容 假设我们有一个富文本编辑器(如TinyMCE、Quill),用户提交的内容是完整的HTML。我们可能希望允许更多格式,但同时要严格限制。
<!-- src/components/RichTextViewer.vue -->
<template>
  <div class="rich-text-viewer" v-html="sanitizedHtml"></div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';
import { useDomPurify } from '@/composables/useDomPurify';

const props = defineProps<{
  html: string;
}>();

const { sanitize, useSanitized } = useDomPurify({
  // 针对富文本,放宽白名单,但增加更严格的属性控制
  ALLOWED_TAGS: [
    'h1', 'h2', 'h3', 'p', 'br', 'b', 'i', 'strong', 'em', 'u', 's', 'blockquote',
    'code', 'pre', 'ul', 'ol', 'li', 'a', 'img', 'span', 'div'
  ],
  ALLOWED_ATTR: ['href', 'title', 'target', 'class', 'style', 'src', 'alt', 'width', 'height'],
  // 强制所有链接在新窗口打开,并添加rel="noopener noreferrer"防止钓鱼
  // 注意:ADD_ATTR需要DOMPurify的特定配置支持,这里演示思路
});
// 或者使用useSanitized
const htmlRef = ref(props.html);
const sanitizedHtml = useSanitized(htmlRef);

// 如果html prop变化
watch(() => props.html, (newVal) => {
  htmlRef.value = newVal;
});
</script>

注意 :对于 target="_blank" 的链接,强烈建议通过DOMPurify的 ADD_ATTR 配置或净化后手动添加 rel="noopener noreferrer" 属性,以防止 window.opener API带来的安全风险。这虽然不属于XSS范畴,但也是重要的安全最佳实践。

4. 高级配置与实战技巧

DOMPurify 的强大在于其丰富的配置。下面分享几个实战中高频使用的配置技巧和注意事项。

4.1 自定义白名单与黑名单

  • 扩展白名单 :如果你的应用需要支持 <table> <iframe> (需极度谨慎)等标签,只需将其加入 ALLOWED_TAGS 数组。
  • 缩减白名单 :为了极致安全,你可以只允许最基本的标签。例如,一个只显示加粗、斜体和链接的评论系统:
    const strictConfig = {
      ALLOWED_TAGS: ['b', 'strong', 'i', 'em', 'a'],
      ALLOWED_ATTR: ['href', 'title'],
    };
    
  • 使用黑名单 FORBID_TAGS FORBID_ATTR 可以明确禁止某些内容,即使它们在白名单中。但 优先使用白名单 是更安全的心态。

4.2 处理样式属性

允许 style 属性存在风险,因为CSS也可以执行脚本(如 expression(...) 旧式IE攻击,或 background: url(javascript:...) )。 DOMPurify 默认会对 style 属性值进行解析和过滤,只允许安全的CSS属性。你可以通过 ALLOWED_ATTR 包含 style ,但务必了解其风险。对于来自不可信源的HTML,最好直接禁止 style

4.3 净化SVG

SVG本身是XML,也可能包含脚本。 DOMPurify 默认支持净化SVG内容。确保你的配置没有意外地禁用相关功能。

4.4 在Node.js环境使用

DOMPurify 需要DOM环境。在Vue3的SSR(服务端渲染)场景下,Node.js中没有 window 对象。你需要创建一个模拟的DOM环境,常用的工具是 jsdom

npm install jsdom

然后,在你的服务端入口文件(如 server.js 或SSR相关文件)中:

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

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

// 现在可以使用purify.sanitize(...)
const clean = purify.sanitize(dirtyHtml);

在你的通用工具函数中,需要做环境判断:

// src/utils/sanitize.ts
import DOMPurify from 'dompurify';

let purify = DOMPurify;
if (typeof window === 'undefined') {
  // 服务端环境
  const { JSDOM } = await import('jsdom');
  const dom = new JSDOM('');
  purify = DOMPurify(dom.window);
}

export const sanitize = (dirty: string) => purify.sanitize(dirty);

4.5 性能考量与缓存

对于高频更新的内容(如实时聊天),频繁调用 sanitize 可能成为性能瓶颈。虽然 DOMPurify 很快,但仍需注意:

  • 对于相同的内容,可以考虑缓存净化结果。
  • 如果内容变化是追加式的(如聊天记录),可以只净化新增的部分。
  • 在Vue的 computed 属性中使用是合理的,因为Vue会进行依赖追踪和缓存。

5. 常见问题、排查技巧与安全边界

即使使用了 DOMPurify ,也并非一劳永逸。以下是我在实践中总结的常见坑点和排查清单。

5.1 净化后样式丢失或布局错乱

  • 问题 :净化后的HTML渲染出来,样式全无,布局混乱。
  • 原因 DOMPurify 默认的白名单非常严格,可能移除了你的HTML中含有的 class style 或特定标签(如 <div> <span> )。
  • 排查
    1. 检查净化前的原始HTML字符串。
    2. 检查你传递给 DOMPurify.sanitize 的配置对象,确认 ALLOWED_TAGS ALLOWED_ATTR 是否包含了所需内容。
    3. 在开发环境下,可以临时将净化后的字符串 console.log 出来,对比净化前后差异。
  • 解决 :根据业务需求,适当扩展白名单配置。如果样式完全由上层CSS类控制,确保 class 属性在 ALLOWED_ATTR 中。

5.2 链接的 target rel 属性处理不当

  • 问题 :净化后的链接点击后,可能在本页打开(导致用户离开你的应用),或者存在 target="_blank" 的安全风险。
  • 解决
    • 如果你想强制所有外链在新窗口打开并添加安全属性,可以在净化后使用DOM操作或字符串处理来批量修改。 DOMPurify RETURN_DOM RETURN_DOM_FRAGMENT 配置可以返回DOM节点,方便操作。
    const clean = DOMPurify.sanitize(dirty, {
      RETURN_DOM_FRAGMENT: true,
      ALLOWED_TAGS: ['a'],
      ALLOWED_ATTR: ['href']
    });
    clean.querySelectorAll('a').forEach(a => {
      a.setAttribute('target', '_blank');
      a.setAttribute('rel', 'noopener noreferrer');
    });
    // 然后将DOM片段插入或转换为字符串
    
    • 更精细的控制(如只对外部链接修改)需要自己解析 href 的域名。

5.3 与Vue的响应式系统结合时出现无限循环

  • 问题 :在 watch computed 中调用净化函数,如果净化函数内部修改了依赖的响应式变量,可能导致无限更新。
  • 解决 :确保净化函数是纯函数,不产生副作用。将待净化的数据作为参数传入,而不是在函数内部读取响应式状态。使用我们上面封装的 useSanitized 可以很好地管理这种依赖关系。

5.4 误以为净化能解决所有安全问题

  • 重要提醒 DOMPurify 解决的是 HTML注入 导致的XSS。它不能防止:
    • 存储型XSS :如果恶意脚本已经通过未净化的输入存入了数据库,净化前端显示只是治标。 净化必须在数据入库前进行,至少要在后端做一次。 前后端双重净化是黄金标准。
    • 基于DOM的XSS :如果JavaScript代码直接使用 innerHTML eval() 等操作未净化的数据, DOMPurify 也帮不上忙。需要避免不安全的DOM操作。
    • 其他Web漏洞 :如SQL注入、CSRF、文件上传漏洞等。

5.5 配置错误导致规则被绕过

  • 问题 :自定义配置时,错误地允许了危险标签或属性。
  • 案例 :为了支持“自定义表情”,允许了 <img> 标签的 onerror 属性。
  • 原则 :遵循最小权限原则。只开放业务必需的功能。每次修改白名单,都要问自己:这个标签/属性是否绝对必要?有没有更安全的替代方案?

5.6 测试你的净化策略

不要相信“应该没问题”。建立测试用例:

  • 单元测试:为你的 sanitize 函数编写测试,输入各种已知的XSS攻击向量,断言输出是安全的或已被移除。
  • 使用在线XSS测试工具或Payload清单进行手动测试。
  • 考虑在代码审查中,将安全配置的变更作为重点审查项。

DOMPurify 集成到Vue3项目中,更像是在数据流动的管道中安装了一个高效可靠的过滤器。它不能替代全面的安全开发意识,但能为你的应用抵御绝大部分前端HTML注入攻击。记住,安全是一个过程,而不是一个产品。保持依赖库的更新,关注安全社区动态,定期审查你的安全配置,才能让你的Vue3应用在享受开发效率的同时,筑起坚固的安全防线。

更多推荐