Skip to content

CSRF 跨站伪造请求

概述

CSRF (Cross-Site Request Forgery) 跨站请求伪造是一种攻击方式,攻击者诱导用户在已登录的网站上执行非预期的操作。

攻击原理

CSRF 攻击的核心是:利用用户已登录的身份,伪造用户请求执行恶意操作

用户登录网站A → 访问攻击者网站B → 网站B自动发送请求到网站A → 网站A执行恶意操作

攻击示例

1. GET 请求攻击

场景:银行转账接口使用 GET 请求

html
<!-- 攻击者网站 -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" style="display:none">

流程

  1. 用户登录了银行网站
  2. 用户访问攻击者网站
  3. 浏览器自动加载图片,发送 GET 请求
  4. 银行服务器执行转账操作

2. POST 请求攻击

场景:银行转账接口使用 POST 请求

html
<!-- 攻击者网站 -->
<form action="https://bank.com/transfer" method="POST" id="stealForm">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="10000">
</form>
<script>
  document.getElementById('stealForm').submit()
</script>

3. 链接攻击

场景:诱导用户点击恶意链接

html
<!-- 攻击者发送的邮件 -->
<a href="https://bank.com/transfer?to=attacker&amount=10000">
  点击领取红包
</a>

攻击条件

CSRF 攻击成功需要满足以下条件:

  1. 用户已登录目标网站

    • 用户的会话未过期
    • 浏览器保存了用户的认证信息
  2. 目标网站没有防护措施

    • 没有验证请求来源
    • 没有使用 CSRF Token
    • 没有使用 SameSite Cookie
  3. 用户访问了攻击者网站

    • 点击了恶意链接
    • 访问了包含恶意代码的页面

防御措施

1. CSRF Token

原理:服务器生成随机 Token,要求请求携带该 Token。

服务器端实现

javascript
// 生成 CSRF Token
function generateCSRFToken() {
  return crypto.randomBytes(32).toString('hex')
}

// 设置 Token 到 Session
app.get('/form', (req, res) => {
  const csrfToken = generateCSRFToken()
  req.session.csrfToken = csrfToken
  
  res.render('form', { csrfToken })
})

// 验证 Token
app.post('/submit', (req, res) => {
  const { csrfToken } = req.body
  const sessionToken = req.session.csrfToken
  
  if (!csrfToken || csrfToken !== sessionToken) {
    return res.status(403).send('CSRF token validation failed')
  }
  
  // 处理请求
  res.send('Success')
})

前端实现

html
<!-- 表单中携带 Token -->
<form action="/submit" method="POST">
  <input type="hidden" name="csrfToken" value="{{ csrfToken }}">
  <input type="text" name="data">
  <button type="submit">提交</button>
</form>
javascript
// AJAX 请求携带 Token
const csrfToken = document.querySelector('meta[name="csrf-token"]').content

fetch('/api/submit', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify(data)
})

原理:限制 Cookie 只在同站请求中发送。

http
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly

SameSite 属性值

  • Strict: 完全禁止跨站发送 Cookie
  • Lax: 允许部分跨站请求(GET 链接、顶级导航)
  • None: 允许跨站发送(需要配合 Secure)

示例

http
// 严格模式 - 最安全
Set-Cookie: sessionId=abc123; SameSite=Strict

// 宽松模式 - 平衡安全和可用性
Set-Cookie: sessionId=abc123; SameSite=Lax

// 允许跨站 - 需要配合 Secure
Set-Cookie: sessionId=abc123; SameSite=None; Secure

3. 验证 Referer

原理:检查请求来源是否为合法域名。

javascript
// 服务器端验证
app.post('/api/action', (req, res) => {
  const referer = req.headers.referer
  
  // 检查 Referer 是否为合法域名
  if (!referer || !referer.startsWith('https://yoursite.com')) {
    return res.status(403).send('Invalid referer')
  }
  
  // 处理请求
  res.send('Success')
})

注意

  • Referer 可以被伪造
  • 某些浏览器或安全软件会阻止 Referer
  • 不能作为唯一的防护措施

原理:将 Token 同时放在 Cookie 和请求参数中。

javascript
// 前端实现
function getCookie(name) {
  const value = `; ${document.cookie}`
  const parts = value.split(`; ${name}=`)
  if (parts.length === 2) return parts.pop().split(';').shift()
}

// 发送请求时携带 Cookie 中的 Token
fetch('/api/action', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    ...data,
    csrf_token: getCookie('csrf_token')
  })
})
javascript
// 服务器端验证
app.post('/api/action', (req, res) => {
  const cookieToken = req.cookies.csrf_token
  const paramToken = req.body.csrf_token
  
  if (!cookieToken || !paramToken || cookieToken !== paramToken) {
    return res.status(403).send('CSRF validation failed')
  }
  
  // 处理请求
  res.send('Success')
})

5. 自定义请求头

原理:跨域请求无法自定义请求头(同源策略)。

javascript
// 前端添加自定义请求头
fetch('/api/action', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest'
  },
  body: JSON.stringify(data)
})
javascript
// 服务器端验证
app.post('/api/action', (req, res) => {
  const requestedWith = req.headers['x-requested-with']
  
  if (!requestedWith || requestedWith !== 'XMLHttpRequest') {
    return res.status(403).send('Invalid request')
  }
  
  // 处理请求
  res.send('Success')
})

防护策略对比

防护措施安全性兼容性实现难度
CSRF Token⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐中等
SameSite Cookie⭐⭐⭐⭐⭐⭐⭐⭐简单
验证 Referer⭐⭐⭐⭐⭐⭐简单
双重 Cookie⭐⭐⭐⭐⭐⭐⭐⭐⭐中等
自定义请求头⭐⭐⭐⭐⭐⭐⭐⭐简单

最佳实践

1. 多重防护

javascript
// 结合多种防护措施
app.post('/api/action', (req, res) => {
  // 1. 验证 CSRF Token
  const csrfToken = req.headers['x-csrf-token']
  if (!validateCSRFToken(csrfToken)) {
    return res.status(403).send('Invalid CSRF token')
  }
  
  // 2. 验证 Referer
  const referer = req.headers.referer
  if (!validateReferer(referer)) {
    return res.status(403).send('Invalid referer')
  }
  
  // 3. 处理请求
  res.send('Success')
})

2. 关键操作二次验证

javascript
// 重要操作要求输入密码或验证码
app.post('/api/transfer', (req, res) => {
  const { password, ...transferData } = req.body
  
  // 验证密码
  if (!validatePassword(req.user, password)) {
    return res.status(403).send('Invalid password')
  }
  
  // 执行转账
  executeTransfer(transferData)
  res.send('Success')
})
http
Set-Cookie: sessionId=abc123; 
  HttpOnly; 
  Secure; 
  SameSite=Strict;
  Path=/;
  Domain=yoursite.com

4. 使用框架内置防护

javascript
// Express - csurf 中间件
const csrf = require('csurf')
app.use(csrf({ cookie: true }))

// Laravel - 内置 CSRF 验证
// 自动验证所有 POST 请求

// Django - 内置 CSRF 中间件
// 自动为表单添加 CSRF Token

CSRF vs XSS

特性CSRFXSS
攻击方式伪造请求注入脚本
是否需要用户登录
是否可以窃取 Cookie
防护措施CSRF Token输出编码
危害程度中等

相关资源

基于 VitePress 的本地知识库