文章

CSRF攻击与防御深度解析:从SameSite Cookie到Token验证的攻防实战

CSRF 跨站请求伪造是前端安全必考题:从攻击链路讲清 SameSite Cookie、Token 校验、Referer 校验等防御手段。 掌握后能现场画出「带 Cookie 的跨站请求如何被伪造与拦截」。

CSRF攻击与防御深度解析:从SameSite Cookie到Token验证的攻防实战

一句话概括

CSRF(跨站请求伪造)利用浏览器自动携带Cookie的机制,在用户不知情时以用户身份发起恶意请求;防御的核心是在”浏览器自动发送的请求”和”用户主动发起的请求”之间建立区分。

核心知识点

1. CSRF 的攻击流程(一句话版)

用户在 bank.com 登录后,Cookie 被浏览器保存。攻击者构造一个页面,其中包含指向 bank.com/transfer?amount=10000&to=attacker 的请求(可以是 form 提交、img src、自动提交的 POST 表单),浏览器访问该页面时自动带上 Cookie,银行以为是用户本人的操作。

1
2
3
4
5
6
<!-- 攻击者只需用户访问这个页面 -->
<form action="https://bank.com/transfer" method="POST" id="hack">
  <input name="amount" value="10000">
  <input name="to" value="attacker_account">
</form>
<script>document.getElementById('hack').submit();</script>

SameSite 控制 Cookie 是否在跨站请求中被发送。Chrome 从 80 版本开始默认 SameSite=Lax。

1
2
3
4
5
6
7
8
// 服务端设置 Cookie
res.cookie('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'Lax'  // 跨站GET导航会发送,POST/iframe不会
});
// 最严格:sameSite: 'Strict' → 任何跨站都不发送
// 但对用户体验有影响(从邮件点链接进网站需要重新登录)

三种模式的行为对比:

模式同站请求跨站GET导航跨站POST/form
None✅ 发送✅ 发送✅ 发送
Lax(默认)✅ 发送✅ 发送❌ 不发送
Strict✅ 发送❌ 不发送❌ 不发送

3. CSRF Token —— 服务端验证方案

服务端生成随机 Token,嵌入页面的表单或 Cookie,提交时验证 Token 一致性。攻击者无法读取跨站页面的 Token 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 服务端:签发和验证
const crypto = require('crypto');

function generateCSRFToken(req) {
  const token = crypto.randomUUID();
  req.session.csrfToken = token;
  return token;
}

function validateCSRFToken(req) {
  const headerToken = req.headers['x-csrf-token'];
  const bodyToken = req.body._csrf;
  // 比较用户提交的 Token 与 session 中存储的 Token
  return (headerToken || bodyToken) === req.session.csrfToken;
}

// Express 中间件
app.use((req, res, next) => {
  if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
    if (!validateCSRFToken(req)) {
      return res.status(403).json({ error: 'CSRF token invalid' });
    }
  }
  next();
});

前端发送 Token:

1
2
3
4
5
6
7
8
9
10
11
// 从 <meta> 标签或 Cookie 读取 Token,通过请求头发送
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify({ amount: 1000, to: 'friend' })
});

当服务端不能存储 Session 时(如 RESTful API),用双重提交 Cookie:Token 同时放在 Cookie 和请求头中,服务端比对两者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 签发:设置一个可以被 JS 读取的 Cookie(不能 httpOnly)
res.cookie('csrf_token', token, {
  sameSite: 'Lax',  // 跨站请求不会携带这个 Cookie
  secure: true
});

// 验证中间件:比较 Cookie 中的 Token 和请求头中的 Token
function doubleSubmitCheck(req, res, next) {
  const cookieToken = req.cookies.csrf_token;
  const headerToken = req.headers['x-csrf-token'];
  if (!cookieToken || cookieToken !== headerToken) {
    return res.status(403).json({ error: 'CSRF check failed' });
  }
  next();
}

5. 综合防护策略(纵深防御)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 生产环境的全链路 CSRF 防护
function csrfProtection(req, res, next) {
  // 1. SameSite Cookie(浏览器强防护)
  // 2. 验证 Origin / Referer 头
  const origin = req.headers.origin;
  if (origin && !allowedOrigins.includes(origin)) {
    return res.status(403).json({ error: 'Invalid origin' });
  }
  // 3. 自定义请求头(非简单请求自动触发 CORS preflight)
  // 4. CSRF Token(关键操作必须验证)
  // 5. 二次确认(转账等高危操作加验证码/密码)
  next();
}

其实你每天都在用

  • 登录任何网站:浏览器自动替你发 Cookie,SameSite=Lax 就是在这个环节保护你的
  • 电信运营商的劫持广告:某些公共 WiFi 会在页面中注入 iframe,如果银行网站没有 SameSite 保护,你的 Cookie 可能被劫持
  • 邮件中点击链接:SameSite=Lax 允许从邮件链到网站的 GET 请求带上 Cookie 正常登录,这正是 Lax 的「人性化」设计
  • 前端 SPA 中 fetch 默认不送 Cookie:fetch 的 credentials 默认是 same-origin,跨站 fetch 不送 Cookie —— 这就是为什么 RESTful API 天然有一定 CSRF 抗性
  • 扫码登录后的跳转:微信扫码登录后跳转回第三方网站,这时候如果 Cookie 是 SameSite=Strict,就会登录失败,所以很多 OAuth 回调页用 SameSite=None

常见误解(FAQ)

❌ 误区:只靠 SameSite Cookie 就能杜绝 CSRF

SameSite 依赖浏览器支持。2026 年 Chrome/Firefox/Safari 都已支持,但旧设备、内嵌 WebView(部分安卓)可能不支持。另外 SameSite=None + Secure 的组合在某些 iframe 场景下也是脆弱的。SameSite 应该作为第一道防线,不是唯一防线。

❌ 误区:RESTful API 不需要防 CSRF

前提不成立。RESTful API 如果只用 Cookie 做身份认证(不用 Authorization Header),仍然需要 CSRF Token。只有完全用 Authorization: Bearer <jwt> 这种需要 JS 显式添加的 Header 做认证,才天然防 CSRF。

❌ 误区:POST 请求才需要防 CSRF,GET 不需要

GET 请求虽然不应该改变数据(RESTful 规范),但旧系统仍然有用 GET 做删除/操作的 API。而且 img 标签的 src 可以发 GET 请求带 Cookie,是天然的 CSRF 向量。

❌ 误区:Referer 头检查是可靠的

Referer 可以被用户或浏览器插件禁用(Referrer-Policy: no-referrer),也可能被中间代理剥离。它应该作为辅助手段,不能作为唯一的 CSRF 防御。

一句话总结

CSRF 的本质是「浏览器替你干了坏事」——防御就是让浏览器区分「用户想干的」和「攻击者诱导的」,SameSite + Token + Origin 三道防线叠加是最佳实践。

本文由作者按照 CC BY 4.0 进行授权