根据 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 漏洞。攻击流程如下:
- 攻击者在自己的 OAuth 应用中预设回调 URL
- 诱导受害者点击授权链接,
state参数被替换为攻击者控制的值 - 受害者授权后,攻击者获得访问受害者账户的 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 验证机制作为后备。
相关工具推荐:
- 🔧 OWASP ZAP —— 开源 Web 安全扫描器,支持 CSRF 自动检测
- 🔧 Burp Suite —— 专业 Web 安全测试工具
- 🔧 jsjson.com CSRF Token 生成器 —— 在线生成密码学安全的随机 Token
- 🔧 jsjson.com JSON 格式化工具 —— 调试 API 请求和响应