CSRF 攻防完全指南:原理、攻击手法与现代防御方案实战

深入解析 CSRF(跨站请求伪造)攻击原理与演变,覆盖 Token 验证、SameSite Cookie、Double Submit、自定义请求头等防御方案,附 Node.js/Java/Python 完整实现与真实攻防案例。

安全与密码 2026-06-08 18 分钟

根据 OWASP 2025 年度报告,CSRF(跨站请求伪造)仍然位列 Web 应用十大安全风险之一。尽管 SameSite Cookie 属性的普及让简单 CSRF 攻击的成功率下降了约 60%,但新型 CSRF 变种攻击(如 JSON-based CSRF、CORS misconfiguration 结合 CSRF)在 2026 年仍然频繁出现在真实漏洞报告中。HackerOne 平台数据显示,CSRF 相关漏洞的平均赏金为 $1,200,部分高影响 CSRF 漏洞赏金超过 $10,000。如果你认为「有了 SameSite Cookie 就万事大吉」,这篇文章会让你重新审视这个判断。

🔐 一、CSRF 攻击原理与真实案例分析

1.1 CSRF 的本质:信任劫持

CSRF 攻击的核心是利用浏览器自动携带 Cookie 的机制,劫持用户已认证的身份。当用户登录 bank.com 后,浏览器会存储一个会话 Cookie。如果用户随后访问恶意网站 evil.com,该网站可以构造一个向 bank.com/transfer 发起的请求——浏览器会自动附带 bank.com 的 Cookie,服务器无法区分这个请求是用户主动发起还是被劫持的。

关键点在于:CSRF 攻击不需要窃取 Cookie,只需要利用 Cookie。攻击者无法读取响应内容,但可以让浏览器以用户身份执行操作。

// ❌ 经典 CSRF 攻击示例 —— 恶意网站中的隐藏表单
// 用户访问 evil.com 时,这段 HTML 会自动提交到 bank.com
const csrfForm = `
<form id="csrf-form" action="https://bank.com/api/transfer" method="POST">
  <input type="hidden" name="to" value="attacker_account" />
  <input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById('csrf-form').submit();</script>
`;

1.2 2026 年 CSRF 攻击的新变种

传统的表单自动提交 CSRF 已经被大多数框架防御,但攻击者已经进化:

变种一:JSON-based CSRF。现代 API 通常使用 JSON 格式,攻击者利用 HTML form 的 enctype 限制,通过 <form>text/plain 编码或 <input> 的特殊构造,绕过 Content-Type 检查。

<!-- ❌ JSON-based CSRF 攻击 —— 利用 text/plain 绕过 Content-Type 检查 -->
<form action="https://api.example.com/admin/delete" method="POST" 
      enctype="text/plain">
  <!-- 构造一个看起来像 JSON 的 payload -->
  <input name='{"userId":"victim","action":"delete","padding":"' 
         value='"}' />
</form>
<script>document.forms[0].submit();</script>

变种二:CORS 配置错误 + CSRF。当服务器配置了 Access-Control-Allow-Origin: * 或不当的 Origin 白名单时,攻击者可以通过 fetch() 发起跨域请求并读取响应,将 CSRF 升级为跨站数据泄露

变种三:子域名 CSRF。如果 *.example.com 下的某个子域名被攻破(如 test.example.com),攻击者可以利用 Cookie 的域作用域(Domain Scope)对 api.example.com 发起 CSRF 攻击。

⚠️ 警告: 不要假设「只有 POST 请求才需要 CSRF 防护」。任何改变服务端状态的请求(POST/PUT/PATCH/DELETE)都需要防护。即使是 GET 请求,如果你的设计允许 GET 触发副作用(这是反模式),也必须防护。

1.3 真实案例:GitHub CSRF 漏洞(CVE-2024-xxxxx 模拟)

2024 年,一个知名开源平台的 OAuth 授权流程被发现存在 CSRF 漏洞。攻击流程如下:

  1. 攻击者在自己的 OAuth 应用中预设回调 URL
  2. 诱导受害者点击授权链接,state 参数被替换为攻击者控制的值
  3. 受害者授权后,攻击者获得访问受害者账户的 Token

根因:OAuth 流程中未正确验证 state 参数——这本质上就是一种 CSRF 攻击。

🛡️ 二、现代 CSRF 防御方案深度对比

2.1 方案对比总览

防御方案 安全性 实现复杂度 浏览器兼容性 适用场景 推荐度
Synchronizer Token ⭐⭐⭐⭐⭐ 所有浏览器 传统表单提交 ✅ 推荐
SameSite Cookie ⭐⭐⭐⭐ Chrome 80+, Firefox 69+, Safari 13+ 现代 Web 应用 ✅ 推荐(需配合其他方案)
Double Submit Cookie ⭐⭐⭐⭐ 所有浏览器 SPA + API 架构 ✅ 推荐
自定义请求头 ⭐⭐⭐⭐ 所有浏览器 纯 API 架构 ✅ 推荐
自定义 Content-Type ⭐⭐⭐ 所有浏览器 JSON API ⚠️ 需配合其他方案
Referer/Origin 校验 ⭐⭐⭐ 所有浏览器 辅助防御 ⚠️ 不可单独使用

📌 记住: 没有任何单一方案能 100% 防御 CSRF。最佳实践是组合使用 2-3 种方案,形成纵深防御。推荐组合:SameSite Cookie + Synchronizer Token + 自定义请求头。

2.2 方案一:Synchronizer Token Pattern(同步令牌模式)

这是最经典、最可靠的 CSRF 防御方案。核心思想是:服务器生成一个随机 Token,嵌入到表单中,提交时验证 Token 是否匹配

// ✅ Node.js + Express 实现 Synchronizer Token Pattern
const crypto = require('crypto');
const express = require('express');
const session = require('express-session');
const app = express();

app.use(session({ secret: 'your-secret-key', resave: false, saveUninitialized: true }));
app.use(express.urlencoded({ extended: true }));

// 生成 CSRF Token 的工具函数
function generateCsrfToken(session) {
  if (!session.csrfToken) {
    // 使用 crypto.randomBytes 生成密码学安全的随机 Token
    session.csrfToken = crypto.randomBytes(32).toString('hex');
  }
  return session.csrfToken;
}

// 中间件:验证 CSRF Token
function csrfProtection(req, res, next) {
  // GET/HEAD/OPTIONS 请求不需要验证
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
    return next();
  }
  
  const token = req.body._csrf || req.headers['x-csrf-token'];
  
  if (!token || !req.session.csrfToken) {
    return res.status(403).json({ error: 'CSRF token missing' });
  }
  
  // 使用 timingSafeEqual 防止时序攻击
  const isValid = crypto.timingSafeEqual(
    Buffer.from(token, 'hex'),
    Buffer.from(req.session.csrfToken, 'hex')
  );
  
  if (!isValid) {
    return res.status(403).json({ error: 'CSRF token invalid' });
  }
  
  next();
}

// 渲染表单页面,嵌入 CSRF Token
app.get('/transfer', (req, res) => {
  const token = generateCsrfToken(req.session);
  res.send(`
    <form action="/api/transfer" method="POST">
      <input type="hidden" name="_csrf" value="${token}" />
      <input type="text" name="to" placeholder="收款账户" />
      <input type="number" name="amount" placeholder="金额" />
      <button type="submit">转账</button>
    </form>
  `);
});

// 受保护的 API 端点
app.post('/api/transfer', csrfProtection, (req, res) => {
  const { to, amount } = req.body;
  // 处理转账逻辑...
  res.json({ success: true, message: `已转账 ${amount} 元到 ${to}` });
});

app.listen(3000, () => console.log('Server running on port 3000'));

⚠️ 警告: crypto.timingSafeEqual 要求两个 Buffer 长度相同。如果 Token 长度不一致,需要先比较长度再进行 timingSafeEqual,否则会抛出异常。这是开发者最常犯的错误之一。

2.3 方案二:SameSite Cookie 属性

SameSite Cookie 是浏览器原生的 CSRF 防御机制,通过控制 Cookie 在跨站请求中的发送行为来阻断 CSRF。

// ✅ SameSite Cookie 配置示例(Express)
const session = require('express-session');

app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,      // 防止 JS 读取(防 XSS 窃取)
    secure: true,        // 仅 HTTPS 传输
    sameSite: 'Lax',     // CSRF 防御的关键设置
    maxAge: 24 * 60 * 60 * 1000  // 24 小时
  }
}));

SameSite 有三个值,选择错误会导致功能异常或安全漏洞:

跨站 GET 请求 跨站 POST 表单 跨站 AJAX 适用场景
Strict ❌ 不发送 ❌ 不发送 ❌ 不发送 银行/金融等高安全场景(但会打断从外部链接跳转后的登录状态)
Lax ✅ 发送(顶级导航) ❌ 不发送 ❌ 不发送 大多数 Web 应用的最佳选择
None ✅ 发送 ✅ 发送 ✅ 发送 跨站嵌入场景(必须配合 Secure

💡 提示: SameSite=Lax 允许顶级导航(如点击链接)携带 Cookie,但阻止 POST 表单和 AJAX 请求携带 Cookie。这意味着从外部链接跳转到你的网站时用户仍然是登录状态,但恶意网站无法通过 POST 表单发起 CSRF 攻击。

SameSite 的局限性:

  • 不防御 XSS 窃取 Cookie(需配合 HttpOnly
  • SameSite=None 的第三方 Cookie 在 Safari 中被默认阻止
  • 子域名之间的请求不被视为「跨站」(需配合域名隔离策略)

2.4 方案三:Double Submit Cookie(双重提交 Cookie)

Double Submit Cookie 适用于无状态 API 架构(不需要服务端 Session),核心思想是:将 CSRF Token 同时放在 Cookie 和请求参数中,服务器比较两者是否一致

// ✅ Node.js 实现 Double Submit Cookie(无状态方案)
const crypto = require('crypto');
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();

app.use(cookieParser('your-cookie-secret'));
app.use(express.json());

// 生成并设置 CSRF Cookie
function setCsrfCookie(res) {
  const token = crypto.randomBytes(32).toString('hex');
  res.cookie('csrf_token', token, {
    httpOnly: false,    // ⚠️ 必须 false,因为 JS 需要读取
    secure: true,
    sameSite: 'Lax',
    maxAge: 3600000     // 1 小时
  });
  return token;
}

// CSRF 验证中间件
function doubleSubmitCsrf(req, res, next) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
    return next();
  }
  
  const cookieToken = req.cookies.csrf_token;
  const headerToken = req.headers['x-csrf-token'];
  
  if (!cookieToken || !headerToken) {
    return res.status(403).json({ error: 'CSRF token missing' });
  }
  
  // 使用 timingSafeEqual 比较,防止时序攻击
  if (cookieToken.length !== headerToken.length ||
      !crypto.timingSafeEqual(
        Buffer.from(cookieToken), 
        Buffer.from(headerToken)
      )) {
    return res.status(403).json({ error: 'CSRF token mismatch' });
  }
  
  next();
}

// API:获取 CSRF Token
app.get('/api/csrf-token', (req, res) => {
  const token = setCsrfCookie(res);
  res.json({ csrfToken: token });
});

// 受保护的 API
app.post('/api/transfer', doubleSubmitCsrf, (req, res) => {
  res.json({ success: true });
});

// 前端使用示例
app.get('/', (req, res) => {
  res.send(`
    <script>
      // 获取 CSRF Token
      fetch('/api/csrf-token').then(r => r.json()).then(({ csrfToken }) => {
        // 后续请求在 Header 中携带 Token
        fetch('/api/transfer', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': csrfToken  // 关键:自定义请求头
          },
          body: JSON.stringify({ to: 'alice', amount: 100 })
        });
      });
    </script>
  `);
});

app.listen(3000);

⚠️ 警告: Double Submit Cookie 的安全性依赖于攻击者无法修改目标域的 Cookie。如果目标域存在 XSS 漏洞,攻击者可以通过 document.cookie 修改 CSRF Cookie,使 Double Submit 失效。因此,Double Submit 必须配合 XSS 防御使用。

2.5 方案四:自定义请求头验证

对于纯 AJAX API 架构,最简单的 CSRF 防御是要求请求携带自定义 Header。这是因为浏览器的同源策略(Same-Origin Policy)禁止跨域 JavaScript 设置自定义请求头(需 CORS 预检)。

// ✅ 自定义请求头 CSRF 防御(最简单的方案)
function customHeaderCsrf(req, res, next) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
    return next();
  }
  
  // 浏览器的同源策略保证:
  // 跨域 JS 无法设置 X-Requested-With 头(除非 CORS 允许)
  const requestedWith = req.headers['x-requested-with'];
  
  if (requestedWith !== 'XMLHttpRequest') {
    return res.status(403).json({ 
      error: 'Missing X-Requested-With header' 
    });
  }
  
  next();
}

// 应用到所有 API 路由
app.use('/api', customHeaderCsrf);

💡 提示: 这个方案的前提是你的 API 没有配置 Access-Control-Allow-Origin: *。如果 CORS 配置允许任意源,攻击者可以通过 fetch() 设置自定义头,此方案失效。正确做法是只允许你的前端域名。

🔧 三、生产环境最佳实践与避坑指南

3.1 框架内置 CSRF 防护

现代 Web 框架通常内置了 CSRF 防护,但默认配置不一定安全:

# ✅ Django CSRF 防护(开箱即用,但需要注意配置)
# settings.py
MIDDLEWARE = [
    'django.middleware.csrf.CsrfViewMiddleware',  # 默认启用
]

# 在模板中使用
# <form method="post">
#   {% csrf_token %}  <!-- Django 自动生成隐藏字段 -->
#   ...
# </form>

# ⚠️ 常见错误:在 API 视图中禁用了 CSRF
# ❌ 错误写法 —— 危险!
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt  # 永远不要在涉及状态变更的视图上使用这个装饰器
def transfer(request):
    pass

# ✅ 正确写法 —— 使用 @ensure_csrf_cookie 并在前端携带 Token
from django.views.decorators.csrf import ensure_csrf_cookie

@ensure_csrf_cookie
def api_transfer(request):
    if request.method == 'POST':
        # Django 自动验证 CSRF Token
        pass
// ✅ Spring Security CSRF 防护(Spring Boot 默认启用)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                // 使用 Cookie 存储 CSRF Token(适合 SPA)
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                // 仅保护状态变更的请求
                .ignoringRequestMatchers("/api/public/**")
            );
        return http.build();
    }
}

// 前端需要在请求头中携带 CSRF Token
// Header 名称默认为 X-CSRF-TOKEN
// Token 从 Cookie XSRF-TOKEN 中读取

3.2 SPA + API 架构的 CSRF 防御方案

对于前后端分离的 SPA 架构,推荐以下组合方案:

// ✅ SPA 架构的 CSRF 防御最佳实践(前端代码)
class ApiClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.csrfToken = null;
  }

  // 初始化:获取 CSRF Token
  async init() {
    const response = await fetch(`${this.baseUrl}/csrf-token`, {
      credentials: 'include'  // 携带 Cookie
    });
    const data = await response.json();
    this.csrfToken = data.token;
  }

  // 发起 API 请求,自动携带 CSRF Token
  async request(method, path, body = null) {
    const options = {
      method,
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': this.csrfToken,      // 自定义头:Token 验证
        'X-Requested-With': 'XMLHttpRequest'  // 自定义头:AJAX 标识
      }
    };

    if (body) {
      options.body = JSON.stringify(body);
    }

    const response = await fetch(`${this.baseUrl}${path}`, options);

    // Token 过期时自动刷新
    if (response.status === 403) {
      const error = await response.json();
      if (error.code === 'CSRF_TOKEN_EXPIRED') {
        await this.init();
        return this.request(method, path, body);
      }
    }

    return response;
  }
}

// 使用
const api = new ApiClient('https://api.example.com');
await api.init();
await api.request('POST', '/transfer', { to: 'alice', amount: 100 });

3.3 避坑指南:常见 CSRF 防御错误

错误一:Token 存储在 localStorage

// ❌ 错误写法 —— localStorage 对 XSS 完全暴露
const csrfToken = localStorage.getItem('csrf_token');
fetch('/api/transfer', {
  headers: { 'X-CSRF-Token': csrfToken }
});

// ✅ 正确写法 —— 使用 Cookie + HttpOnly 存储
// CSRF Token 通过 Set-Cookie 由服务端设置
// 前端从 Cookie 中读取(需要 httpOnly: false)
// 或者通过专门的 API 端点获取

错误二:GET 请求触发副作用

// ❌ 错误写法 —— GET 请求不应该改变状态
app.get('/api/delete/:id', (req, res) => {
  // 攻击者可以通过 <img src="https://api.example.com/delete/123"> 
  // 触发删除操作
  deleteUser(req.params.id);
  res.json({ success: true });
});

// ✅ 正确写法 —— 状态变更操作使用 POST/DELETE
app.delete('/api/users/:id', csrfProtection, (req, res) => {
  deleteUser(req.params.id);
  res.json({ success: true });
});

错误三:CORS 配置过于宽松

// ❌ 错误写法 —— 允许任意源
app.use(cors({
  origin: '*',  // 危险!允许任何网站跨域请求
  credentials: true
}));

// ✅ 正确写法 —— 白名单验证 Origin
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];

app.use(cors({
  origin: function(origin, callback) {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true
}));

3.4 测试你的 CSRF 防御

使用以下 Python 脚本自动化测试 CSRF 防御是否有效:

# csrf_test.py — 自动化 CSRF 防御测试
import requests

BASE_URL = "http://localhost:3000"

def test_csrf_protection():
    session = requests.Session()
    
    # 1. 先登录获取会话
    login_resp = session.post(f"{BASE_URL}/api/login", json={
        "username": "testuser",
        "password": "password123"
    })
    assert login_resp.status_code == 200, "Login failed"
    
    # 2. 测试无 CSRF Token 的 POST 请求应被拒绝
    no_token_resp = session.post(f"{BASE_URL}/api/transfer", json={
        "to": "attacker",
        "amount": 10000
    })
    assert no_token_resp.status_code == 403, \
        f"CSRF protection failed: got {no_token_resp.status_code}, expected 403"
    print("✅ Test 1 PASSED: POST without CSRF token rejected")
    
    # 3. 测试伪造的 CSRF Token 应被拒绝
    fake_token_resp = session.post(f"{BASE_URL}/api/transfer", 
        json={"to": "attacker", "amount": 10000},
        headers={"X-CSRF-Token": "fake-token-12345"}
    )
    assert fake_token_resp.status_code == 403, \
        "CSRF protection failed: fake token accepted"
    print("✅ Test 2 PASSED: POST with fake CSRF token rejected")
    
    # 4. 测试获取合法 Token 后请求应成功
    csrf_resp = session.get(f"{BASE_URL}/api/csrf-token")
    csrf_token = csrf_resp.json().get("csrfToken") or \
                 csrf_resp.json().get("token")
    assert csrf_token, "Failed to get CSRF token"
    
    valid_resp = session.post(f"{BASE_URL}/api/transfer",
        json={"to": "alice", "amount": 100},
        headers={"X-CSRF-Token": csrf_token}
    )
    assert valid_resp.status_code == 200, \
        f"Valid CSRF request failed: got {valid_resp.status_code}"
    print("✅ Test 3 PASSED: POST with valid CSRF token accepted")
    
    # 5. 测试跨域 Referer 缺失应被拒绝(如果启用了 Referer 检查)
    # 此测试取决于是否启用了 Referer/Origin 校验
    no_referer_resp = session.post(f"{BASE_URL}/api/transfer",
        json={"to": "attacker", "amount": 10000},
        headers={
            "X-CSRF-Token": csrf_token,
            "Referer": ""  # 空 Referer
        }
    )
    print(f"ℹ️  Test 5: Empty Referer got status {no_referer_resp.status_code}")
    
    print("\n🎉 All CSRF protection tests completed!")

if __name__ == "__main__":
    test_csrf_protection()

3.5 安全头强化:多层防御

CSRF 防御不应孤立存在,需要配合其他安全头形成纵深防御:

# ✅ Nginx 安全头配置 —— 纵深防御
server {
    listen 443 ssl;
    
    # CSRF 相关安全头
    add_header X-Frame-Options "SAMEORIGIN" always;           # 防止 Clickjacking
    add_header X-Content-Type-Options "nosniff" always;       # 防止 MIME 嗅探
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;  # 控制 Referer
    
    # CSP —— 阻止 XSS(CSRF 的最佳搭档)
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;
    
    # HSTS —— 强制 HTTPS(防止 SSL 剥离攻击)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}

💡 四、总结与防御决策树

CSRF 防御的核心原则是纵深防御。根据你的架构选择合适的组合:

架构类型 推荐防御组合 优先级
传统服务端渲染(SSR) Synchronizer Token + SameSite=Lax Token 为主,SameSite 为辅
SPA + API(同域) Double Submit Cookie + 自定义请求头 + SameSite=Lax 三者组合
SPA + API(跨域) Synchronizer Token + CORS 白名单 + SameSite=None+Secure Token 为主,CORS 严格
移动端 API 自定义请求头 + API Key/Token 认证 无需 Cookie-based CSRF

关键结论: CSRF 防御不是一次性配置,而是持续的安全工程。定期审计 CORS 配置、Cookie 属性和 API 端点的 CSRF 覆盖率,是保持安全的关键。建议将 CSRF 测试集成到 CI/CD 流程中,使用 OWASP ZAP 或 Burp Suite 自动化扫描。

📌 记住: SameSite Cookie 是优秀的第一道防线,但绝不能单独依赖它。浏览器兼容性问题、子域名攻击、以及 SameSite=None 的第三方 Cookie 限制,都意味着你需要至少一种服务端 CSRF 验证机制作为后备。

相关工具推荐:

📚 相关文章