概述

跨站请求伪造(Cross-Site Request Forgery,简称CSRF)是Web应用中常见的安全漏洞之一。攻击者利用用户已登录的身份,诱导用户执行非预期的操作,从而达到篡改数据、转移资金等恶意目的。本集将深入探讨CSRF攻击的原理、危害以及Vue 3应用中的防御策略,帮助开发者构建更加安全的Vue 3应用。

一、CSRF攻击基础概念

1.1 CSRF攻击的定义

CSRF攻击是一种网络攻击方式,攻击者通过诱导用户访问恶意网站或点击恶意链接,利用用户已登录的身份,在用户不知情的情况下,向目标网站发送伪造的请求,从而执行非预期的操作。

1.2 CSRF攻击的工作原理

  1. 用户登录目标网站A,获取身份凭证(如Cookie、Token)
  2. 目标网站A将身份凭证存储在用户浏览器中
  3. 用户在未登出网站A的情况下,访问恶意网站B
  4. 恶意网站B向网站A发送伪造的请求,请求中包含恶意操作(如转账、修改密码等)
  5. 用户浏览器携带网站A的身份凭证,向网站A发送请求
  6. 网站A验证身份凭证有效,执行恶意操作

1.3 CSRF攻击的特点

  • 利用用户身份:攻击者不需要获取用户的凭证,只需利用用户已登录的状态
  • 跨站执行:攻击请求来自第三方网站,而非用户直接操作
  • 难以检测:攻击请求与正常请求在外观上没有区别
  • 危害巨大:可导致用户数据被篡改、资金损失等严重后果

二、CSRF攻击的类型

2.1 GET型CSRF

GET型CSRF攻击是指攻击者通过诱导用户点击包含恶意GET请求的链接,执行非预期操作。

攻击示例

<!-- 恶意网站B中的HTML -->
<img src="https://bank.example.com/transfer?to=attacker&amount=10000" style="display:none;">

当用户访问恶意网站B时,浏览器会自动发送GET请求到银行网站,执行转账操作。

2.2 POST型CSRF

POST型CSRF攻击是指攻击者通过表单提交包含恶意操作的POST请求。

攻击示例

<!-- 恶意网站B中的HTML -->
<form action="https://bank.example.com/transfer" method="POST" id="csrf-form">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="10000">
</form>
<script>
document.getElementById('csrf-form').submit();
</script>

当用户访问恶意网站B时,浏览器会自动提交表单,执行转账操作。

2.3 链接型CSRF

链接型CSRF攻击是指攻击者通过社交媒体、邮件等方式,诱导用户点击包含恶意请求的链接。

攻击示例

https://bank.example.com/transfer?to=attacker&amount=10000

当用户点击该链接时,如果用户已登录银行网站,就会执行转账操作。

三、CSRF攻击的危害

3.1 用户数据被篡改

攻击者可以通过CSRF攻击修改用户的个人信息、密码、邮箱等敏感数据。

3.2 资金损失

对于金融类网站,攻击者可以通过CSRF攻击执行转账、购买等操作,导致用户资金损失。

3.3 权限提升

攻击者可以通过CSRF攻击修改用户权限,提升为管理员权限,从而进一步控制网站。

3.4 数据泄露

攻击者可以通过CSRF攻击获取用户的敏感数据,如个人信息、交易记录等。

3.5 网站声誉受损

CSRF攻击可能导致网站用户数据泄露、资金损失等问题,严重影响网站的声誉和用户信任。

四、CSRF攻击的防御原理

4.1 同源策略

同源策略是浏览器的安全机制,限制不同源的网页之间的交互。但同源策略并不能完全防止CSRF攻击,因为GET请求、表单提交等不受同源策略限制。

4.2 验证请求来源

通过验证请求的Origin或Referer头,可以判断请求是否来自合法的来源。

4.3 随机令牌验证

为每个请求添加随机生成的令牌(Token),服务器验证令牌的有效性,从而防止CSRF攻击。

设置Cookie的SameSite属性,限制Cookie的发送范围,从而防止跨站请求携带Cookie。

五、Vue 3中的CSRF防护措施

SameSite Cookie是最有效的CSRF防御措施之一,通过设置Cookie的SameSite属性,可以限制Cookie在跨站请求中的发送。

SameSite属性值

  • Strict:只允许同源请求携带Cookie
  • Lax:允许部分跨站请求携带Cookie(如GET请求)
  • None:允许所有跨站请求携带Cookie,但必须同时设置Secure属性

设置示例

Set-Cookie: sessionId=123456; HttpOnly; Secure; SameSite=Strict

Vue 3项目中配置

  1. 前端配置:Vue 3本身不需要特殊配置,由后端设置Cookie属性
  2. 后端配置
    • Node.js/Express:
      const express = require('express');
      const app = express();
      
      app.use((req, res, next) => {
        res.cookie('sessionId', '123456', {
          httpOnly: true,
          secure: true,
          sameSite: 'strict'
        });
        next();
      });
    • Django:
      # settings.py
      SESSION_COOKIE_SAMESITE = 'Strict'
      CSRF_COOKIE_SAMESITE = 'Strict'

5.2 CSRF Token

CSRF Token是一种常用的防御措施,通过为每个请求添加随机生成的令牌,服务器验证令牌的有效性,从而防止CSRF攻击。

实现原理

  1. 服务器生成随机令牌,存储在用户会话中
  2. 服务器将令牌发送给前端,前端存储令牌
  3. 前端发送请求时,将令牌添加到请求中
  4. 服务器验证令牌的有效性,只有令牌有效才执行请求

Vue 3中实现CSRF Token

5.2.1 后端生成并返回Token

Node.js/Express示例

const express = require('express');
const session = require('express-session');
const crypto = require('crypto');

const app = express();

// 配置会话
app.use(session({
  secret: 'secret_key',
  resave: false,
  saveUninitialized: true
}));

// 生成CSRF Token
app.use((req, res, next) => {
  // 生成随机Token
  const csrfToken = crypto.randomBytes(32).toString('hex');
  // 存储到会话中
  req.session.csrfToken = csrfToken;
  // 返回给前端
  res.locals.csrfToken = csrfToken;
  next();
});

// 验证CSRF Token
app.post('/protected', (req, res) => {
  const { csrfToken } = req.body;
  if (csrfToken && csrfToken === req.session.csrfToken) {
    // Token验证通过,执行操作
    res.json({ success: true });
  } else {
    // Token验证失败
    res.status(403).json({ success: false, message: 'CSRF Token验证失败' });
  }
});

5.2.2 前端存储并发送Token

Vue 3组件示例

<template>
  <form @submit.prevent="handleSubmit">
    <input type="hidden" :value="csrfToken" name="csrfToken">
    <input type="text" v-model="data" placeholder="请输入数据">
    <button type="submit">提交</button>
  </form>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'

const csrfToken = ref('')
const data = ref('')

// 获取CSRF Token
onMounted(async () => {
  const response = await axios.get('/csrf-token')
  csrfToken.value = response.data.csrfToken
})

// 提交表单
const handleSubmit = async () => {
  try {
    const response = await axios.post('/protected', {
      csrfToken: csrfToken.value,
      data: data.value
    })
    console.log('提交成功', response.data)
  } catch (error) {
    console.error('提交失败', error)
  }
}
</script>

5.2.3 Axios全局配置

配置Axios自动添加CSRF Token

// src/utils/axios.js
import axios from 'axios'

const instance = axios.create({
  baseURL: '/api',
  timeout: 10000
})

// 从Cookie中获取CSRF Token
const getCsrfTokenFromCookie = () => {
  const match = document.cookie.match(/(^|;)\s*csrfToken\s*=\s*([^;]+)/)
  return match ? match[2] : null
}

// 请求拦截器,自动添加CSRF Token
instance.interceptors.request.use(config => {
  const token = getCsrfTokenFromCookie()
  if (token) {
    // 添加到请求头
    config.headers['X-CSRF-Token'] = token
    // 或添加到请求体
    if (config.method === 'post' || config.method === 'put' || config.method === 'delete') {
      config.data = config.data || {}
      config.data.csrfToken = token
    }
  }
  return config
}, error => {
  return Promise.reject(error)
})

export default instance

5.3 验证Origin/Referer头

通过验证请求的Origin或Referer头,可以判断请求是否来自合法的来源。

实现原理

  1. 浏览器在发送跨站请求时,会自动添加Origin或Referer头
  2. 服务器验证Origin或Referer头是否来自合法的域名
  3. 只有验证通过的请求才会被执行

Vue 3项目中实现

后端验证示例(Node.js/Express)

app.use((req, res, next) => {
  const origin = req.headers.origin;
  const referer = req.headers.referer;
  const allowedOrigins = ['https://example.com', 'https://www.example.com'];
  
  // 验证Origin头
  if (origin && allowedOrigins.includes(origin)) {
    return next();
  }
  
  // 验证Referer头
  if (referer) {
    const refererOrigin = new URL(referer).origin;
    if (allowedOrigins.includes(refererOrigin)) {
      return next();
    }
  }
  
  // 验证失败
  res.status(403).json({ success: false, message: '请求来源验证失败' });
});

5.4 双重提交Cookie

双重提交Cookie是一种无需服务器存储Token的防御措施,通过将Token同时存储在Cookie和请求中,服务器验证两者是否一致。

实现原理

  1. 服务器生成随机Token,存储在Cookie中
  2. 前端从Cookie中获取Token,添加到请求中
  3. 服务器验证请求中的Token与Cookie中的Token是否一致
  4. 只有一致的请求才会被执行

Vue 3中实现双重提交Cookie

5.4.1 后端生成Token

Node.js/Express示例

const express = require('express');
const crypto = require('crypto');

const app = express();

// 生成CSRF Token并存储在Cookie中
app.use((req, res, next) => {
  // 检查是否已存在Token
  if (!req.cookies.csrfToken) {
    // 生成随机Token
    const csrfToken = crypto.randomBytes(32).toString('hex');
    // 存储在Cookie中
    res.cookie('csrfToken', csrfToken, {
      httpOnly: false, // 允许前端访问
      secure: true,
      sameSite: 'strict'
    });
  }
  next();
});

// 验证CSRF Token
app.post('/protected', (req, res) => {
  const { csrfToken } = req.body;
  const cookieToken = req.cookies.csrfToken;
  
  if (csrfToken && csrfToken === cookieToken) {
    // Token验证通过,执行操作
    res.json({ success: true });
  } else {
    // Token验证失败
    res.status(403).json({ success: false, message: 'CSRF Token验证失败' });
  }
});

5.4.2 前端实现

Vue 3组件示例

<template>
  <button @click="handleSubmit">执行操作</button>
</template>

<script setup>
import { ref } from 'vue'
import axios from 'axios'

// 从Cookie中获取CSRF Token
const getCsrfToken = () => {
  const match = document.cookie.match(/(^|;)\s*csrfToken\s*=\s*([^;]+)/)
  return match ? match[2] : null
}

// 提交请求
const handleSubmit = async () => {
  try {
    const csrfToken = getCsrfToken()
    const response = await axios.post('/protected', {
      csrfToken: csrfToken
    })
    console.log('操作成功', response.data)
  } catch (error) {
    console.error('操作失败', error)
  }
}
</script>

5.5 自定义请求头

通过添加自定义请求头,可以防止CSRF攻击,因为浏览器在发送跨站请求时,不会自动添加自定义请求头。

实现原理

  1. 前端发送请求时,添加自定义请求头
  2. 服务器验证请求中是否包含自定义请求头
  3. 只有包含自定义请求头的请求才会被执行

Vue 3中实现

Axios配置示例

// src/utils/axios.js
import axios from 'axios'

const instance = axios.create({
  baseURL: '/api',
  timeout: 10000,
  headers: {
    'X-Requested-With': 'XMLHttpRequest' // 添加自定义请求头
  }
})

export default instance

后端验证示例

app.use((req, res, next) => {
  if (req.headers['x-requested-with'] === 'XMLHttpRequest') {
    return next();
  }
  res.status(403).json({ success: false, message: '请求头验证失败' });
});

六、Vue 3 CSRF防护最佳实践

6.1 结合多种防御措施

单一的防御措施可能存在漏洞,建议结合多种防御措施,如SameSite Cookie + CSRF Token,以提高安全性。

6.2 对敏感操作加强防护

对于敏感操作(如转账、修改密码、删除数据等),应加强防护措施,如:

  • 使用Strict模式的SameSite Cookie
  • 要求用户重新验证身份(如输入密码、验证码等)
  • 添加操作确认机制

6.3 合理使用HTTP方法

遵循RESTful API设计原则,合理使用HTTP方法:

  • GET请求只用于获取数据,不执行修改操作
  • POST/PUT/DELETE请求用于修改数据
  • 避免使用GET请求执行敏感操作

6.4 保持库和框架更新

定期更新Vue 3及相关库(如Axios),修复已知的安全漏洞。

6.5 实施内容安全策略(CSP)

实施严格的CSP策略,可以防止恶意脚本的执行,从而减轻CSRF攻击的影响。

CSP配置示例

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; object-src 'none';

6.6 教育用户安全意识

提高用户的安全意识,避免点击未知链接、访问不可信网站,定期登出敏感网站。

七、CSRF攻击案例分析

7.1 案例:某银行网站CSRF攻击

背景:某银行网站存在CSRF漏洞,攻击者通过CSRF攻击窃取用户资金。

攻击过程

  1. 用户登录银行网站,获取身份凭证
  2. 攻击者发送包含恶意链接的邮件给用户
  3. 用户点击恶意链接,访问恶意网站
  4. 恶意网站向银行网站发送转账请求,请求中包含攻击者的账户和转账金额
  5. 用户浏览器携带银行网站的身份凭证,向银行网站发送请求
  6. 银行网站验证身份凭证有效,执行转账操作

防御措施

  1. 实施SameSite Cookie
  2. 添加CSRF Token验证
  3. 对敏感操作要求二次验证
  4. 实施内容安全策略

7.2 案例:某社交平台CSRF攻击

背景:某社交平台存在CSRF漏洞,攻击者通过CSRF攻击发布恶意内容。

攻击过程

  1. 用户登录社交平台,获取身份凭证
  2. 攻击者创建包含恶意表单的网站
  3. 用户访问恶意网站,表单自动提交
  4. 恶意表单向社交平台发送发布内容请求,内容包含恶意链接
  5. 社交平台验证身份凭证有效,发布恶意内容

防御措施

  1. 实施CSRF Token验证
  2. 验证请求的Origin/Referer头
  3. 对发布内容进行审核

八、Vue 3 CSRF防护代码实践

8.1 创建CSRF防护的Axios实例

// src/utils/axios.js
import axios from 'axios'

// 创建Axios实例
const api = axios.create({
  baseURL: '/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest'
  }
})

// 从Cookie中获取CSRF Token
const getCsrfToken = () => {
  const match = document.cookie.match(/(^|;)\s*csrfToken\s*=\s*([^;]+)/)
  return match ? match[2] : null
}

// 请求拦截器,添加CSRF Token
api.interceptors.request.use(config => {
  // 获取CSRF Token
  const csrfToken = getCsrfToken()
  
  // 添加到请求头
  if (csrfToken) {
    config.headers['X-CSRF-Token'] = csrfToken
  }
  
  // 添加到请求体
  if (['post', 'put', 'delete'].includes(config.method) && config.data) {
    config.data = { ...config.data, csrfToken }
  }
  
  return config
}, error => {
  return Promise.reject(error)
})

// 响应拦截器,处理错误
api.interceptors.response.use(response => {
  return response
}, error => {
  if (error.response && error.response.status === 403) {
    console.error('CSRF验证失败:', error.response.data.message)
    // 可以添加重定向到登录页或显示错误信息
  }
  return Promise.reject(error)
})

export default api

8.2 创建CSRF防护的表单组件

<template>
  <form @submit.prevent="handleSubmit" class="csrf-form">
    <!-- 隐藏的CSRF Token字段 -->
    <input type="hidden" :value="csrfToken" name="csrfToken">
    
    <!-- 表单内容 -->
    <slot name="fields"></slot>
    
    <!-- 提交按钮 -->
    <div class="form-actions">
      <slot name="actions">
        <button type="submit" class="submit-btn">提交</button>
      </slot>
    </div>
  </form>
</template>

<script setup>
import { ref, onMounted, watch } from 'vue'
import api from '@/utils/axios'

const props = defineProps({
  // 是否自动获取CSRF Token
  autoFetchToken: {
    type: Boolean,
    default: true
  },
  // 自定义CSRF Token
  token: {
    type: String,
    default: ''
  }
})

const emit = defineEmits(['submit', 'error'])

const csrfToken = ref(props.token)
const isLoading = ref(false)

// 获取CSRF Token
const fetchCsrfToken = async () => {
  try {
    const response = await api.get('/csrf-token')
    csrfToken.value = response.data.csrfToken
  } catch (error) {
    emit('error', error)
    console.error('获取CSRF Token失败:', error)
  }
}

// 监听token属性变化
watch(() => props.token, (newToken) => {
  if (newToken) {
    csrfToken.value = newToken
  }
})

// 组件挂载时获取Token
onMounted(() => {
  if (props.autoFetchToken && !props.token) {
    fetchCsrfToken()
  }
})

// 处理表单提交
const handleSubmit = async (event) => {
  isLoading.value = true
  
  try {
    // 触发submit事件,传递表单数据和CSRF Token
    emit('submit', {
      csrfToken: csrfToken.value,
      event
    })
  } catch (error) {
    emit('error', error)
    console.error('表单提交失败:', error)
  } finally {
    isLoading.value = false
  }
}

// 暴露方法
defineExpose({
  fetchCsrfToken,
  isLoading
})
</script>

<style scoped>
.csrf-form {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.form-actions {
  margin-top: 20px;
  text-align: right;
}

.submit-btn {
  padding: 10px 20px;
  background-color: #409eff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.submit-btn:hover {
  background-color: #66b1ff;
}

.submit-btn:disabled {
  background-color: #a0cfff;
  cursor: not-allowed;
}
</style>

8.3 使用CSRF防护的表单组件

<template>
  <div class="app">
    <h1>CSRF防护示例</h1>
    
    <CsrfForm @submit="handleSubmit" @error="handleError" ref="csrfFormRef">
      <template #fields>
        <div class="form-group">
          <label for="username">用户名</label>
          <input type="text" id="username" v-model="formData.username" class="form-input">
        </div>
        
        <div class="form-group">
          <label for="email">邮箱</label>
          <input type="email" id="email" v-model="formData.email" class="form-input">
        </div>
      </template>
      
      <template #actions>
        <button 
          type="submit" 
          class="submit-btn" 
          :disabled="csrfFormRef?.isLoading"
        >
          {{ csrfFormRef?.isLoading ? '提交中...' : '提交' }}
        </button>
      </template>
    </CsrfForm>
    
    <div v-if="message" :class="['message', message.type]">
      {{ message.text }}
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import CsrfForm from '@/components/CsrfForm.vue'
import api from '@/utils/axios'

const csrfFormRef = ref(null)
const formData = ref({
  username: '',
  email: ''
})
const message = ref(null)

// 处理表单提交
const handleSubmit = async ({ csrfToken }) => {
  try {
    const response = await api.post('/update-profile', {
      ...formData.value,
      csrfToken
    })
    
    message.value = {
      type: 'success',
      text: '个人信息更新成功'
    }
  } catch (error) {
    message.value = {
      type: 'error',
      text: '个人信息更新失败: ' + (error.response?.data?.message || error.message)
    }
  }
}

// 处理错误
const handleError = (error) => {
  message.value = {
    type: 'error',
    text: '操作失败: ' + error.message
  }
}
</script>

<style scoped>
.app {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.form-group {
  margin-bottom: 15px;
}

.form-input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.message {
  margin-top: 20px;
  padding: 10px;
  border-radius: 4px;
}

.message.success {
  background-color: #f0f9eb;
  color: #67c23a;
  border: 1px solid #e1f3d8;
}

.message.error {
  background-color: #fef0f0;
  color: #f56c6c;
  border: 1px solid #fbc4c4;
}
</style>

九、总结与展望

CSRF攻击是Web应用中常见的安全漏洞之一,对Vue 3应用的安全性构成严重威胁。通过了解CSRF攻击的原理、类型和危害,以及Vue 3中的防护措施,开发者可以构建更加安全的Vue 3应用。

防御CSRF攻击的核心原则

  1. 限制Cookie的发送范围:实施SameSite Cookie
  2. 验证请求的合法性:添加CSRF Token验证
  3. 验证请求来源:检查Origin/Referer头
  4. 结合多种防御措施:提高安全性
  5. 对敏感操作加强防护:要求二次验证

未来发展趋势

  • 浏览器原生支持的防护机制将不断增强
  • 开发框架和库将提供更强大的内置安全防护
  • AI技术将被用于自动检测和防御CSRF攻击
  • 安全开发理念将更加深入人心

通过遵循安全最佳实践,结合Vue 3的防护机制和手动防御策略,开发者可以有效防止CSRF攻击,保护用户数据安全和应用程序的完整性。

参考资料

  1. Vue 3官方文档 - 安全
  2. OWASP CSRF攻击防御 cheat sheet
  3. MDN Web文档 - SameSite Cookie
  4. Axios官方文档
  5. OWASP Top 10

扩展学习

  • 学习OWASP Top 10安全漏洞
  • 掌握内容安全策略(CSP)的详细配置
  • 了解其他Web安全漏洞,如XSS、SQL注入等
  • 学习使用安全扫描工具,如OWASP ZAP、Burp Suite等
  • 参与开源项目的安全审计

下一集预告:我们将继续探讨Vue 3应用的安全防护,重点介绍数据加密与安全传输的原理和实现方法。

« 上一篇 221-vue3-xss-attacks-and-defense 下一篇 » Vue 3 数据加密与安全传输:保护用户敏感信息