CSRF 防护
本页提供:CSRF Token 服务端完整实现(生成/验证/轮换代码)、SameSite Cookie 配置示例与浏览器兼容性说明、Double Submit Cookie 模式实现、Express/Django/Spring 三种框架的 CSRF 配置,以及交互式审查面清单。
SKILL 应区分「浏览器携带 Cookie 的站点」与「仅 Authorization 的 API」:前者必须覆盖 CSRF 令牌或等价非简单头;后者 CSRF 面通常更小,但仍需避免误用 Cookie 混绑。
CSRF Token 完整实现(生成/验证/轮换)
// csrf-token.ts — Node.js 完整实现
import { randomBytes, timingSafeEqual } from 'crypto';
import { Request, Response, NextFunction } from 'express';
const CSRF_TOKEN_LENGTH = 32;
const CSRF_HEADER = 'x-csrf-token';
const CSRF_COOKIE = 'csrf_token';
// 生成安全随机 CSRF token
export function generateCsrfToken(): string {
return randomBytes(CSRF_TOKEN_LENGTH).toString('hex');
}
// 中间件:校验 CSRF token(同步令牌模式)
export function csrfProtect(req: Request, res: Response, next: NextFunction) {
// GET/HEAD/OPTIONS 不需要 CSRF 保护
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
const sessionToken = req.session?.csrfToken as string | undefined;
const requestToken = (req.headers[CSRF_HEADER] as string)
|| req.body?._csrf;
if (!sessionToken || !requestToken) {
return res.status(403).json({ error: 'CSRF token missing' });
}
// 常量时间比较,防止时序攻击
const sessionBuf = Buffer.from(sessionToken, 'utf8');
const requestBuf = Buffer.from(requestToken, 'utf8');
if (sessionBuf.length !== requestBuf.length ||
!timingSafeEqual(sessionBuf, requestBuf)) {
return res.status(403).json({ error: 'CSRF token invalid' });
}
// 轮换 token(每次请求后更新,防重放)
req.session.csrfToken = generateCsrfToken();
next();
}
// 初始化:登录后在 session 中生成 token 并种 cookie
export function initCsrfToken(req: Request, res: Response) {
const token = generateCsrfToken();
req.session.csrfToken = token;
res.cookie(CSRF_COOKIE, token, {
httpOnly: false, // 前端 JS 需要读取
secure: process.env.NODE_ENV === 'production',
sameSite: 'Strict',
});
return token;
}
常见框架 CSRF 配置
# Django — settings.py(默认开启,需确认以下配置)
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware', # 必须在列表中
# ...
]
CSRF_COOKIE_SECURE = True # 仅 HTTPS
CSRF_COOKIE_HTTPONLY = False # 前端 JS 需读取(Double Submit 模式)
CSRF_COOKIE_SAMESITE = 'Strict'
CSRF_TRUSTED_ORIGINS = ['https://app.example.com'] # Django 4.0+ 必须配置
// Spring Security — CSRF 配置(Java)
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
)
.sessionManagement(sm -> sm
.sessionFixation().newSession() // 登录后轮换 session
);
return http.build();
}
}
Double Submit Cookie 模式实现
// 客户端:从 cookie 中读取 token,放入请求头
function getCookie(name) {
const match = document.cookie.match(new RegExp(`(^|; )${name}=([^;]+)`));
return match ? decodeURIComponent(match[2]) : null;
}
// 所有非安全方法请求自动附加 CSRF token
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCookie('csrf_token'), // 从 cookie 读取
},
body: JSON.stringify({ amount: 100, to: 'user-123' }),
credentials: 'same-origin',
});
// 服务端校验:比较 cookie 值与 header 值
function doubleCsrfCheck(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 || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
next();
}
有状态请求校验流
[ 浏览器 / 客户端发起变更请求 ]
│
▼
┌──────────────────────┐
│ 方法语义:非 GET/HEAD │──── GET 不得改状态
└──────────────────────┘
│
▼
┌──────────────────────┐
│ 来源可信:token / │──── 表单:同步 token;API:自定义头或
│ 非简单头 / 二次确认 │ Bearer;防 XSS 先于双重 Cookie
└──────────────────────┘
│
▼
┌──────────────────────┐
│ Cookie:SameSite 与 │──── Lax/Strict/None+Secure;
│ 会话固定 / 登录后轮换 │ 跨站子域策略单独评估
└──────────────────────┘
│
▼
[ 执行业务逻辑 ]
顺序要点:先保证方法与路由设计正确,再叠令牌与 Cookie 策略;OAuth 等重定向流用 state 防 CSRF,与表单 token 不同轨但同属「不可猜测参数」家族。
同步令牌与自定义头
经典服务端渲染表单使用同步 CSRF token(会话绑定、一次性或旋转策略按威胁模型选)。双重提交 cookie 需配合严格 XSS 治理,否则窃 cookie 即绕过。
SPA 常用 Authorization: Bearer 或 X-Requested-With 等非简单头触发 CORS 预检,使跨站页面难以静默发起同等请求;须与「本页 CORS 技能」白名单一致,禁止反射 Origin。
- 令牌:加密随机、足够熵;服务端校验常量时间比较(防时序旁路)。
- 上传与 multipart:token 字段或头与框架约定对齐。
SameSite Cookie 配置与浏览器兼容性
// Express.js — 完整安全 Cookie 配置示例
const sessionConfig = {
name: 'sid',
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
// SameSite 选择逻辑:
// - Strict:最强,邮件内直链跳转时 cookie 不带(可能影响 UX)
// - Lax(推荐默认):顶层 GET 导航带 cookie,跨站 POST/XHR 不带
// - None:需跨站嵌入时使用,必须配合 Secure=true
sameSite: process.env.CROSS_SITE_EMBED ? 'None' : 'Lax',
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 3600000, // 1 小时
}
};
// 浏览器支持情况与回退策略
// Chrome 80+:SameSite=Lax 为默认值(未设置时)
// Safari 12:不支持 SameSite=None,会忽略该 cookie
// IE11/Edge Legacy:不支持 SameSite,完全忽略
//
// 回退策略:SameSite=None 时同时设 Secure,旧浏览器降级为无 SameSite
// 检测方法:User-Agent 检测后根据浏览器版本决定是否发送 None
# Nginx — Set-Cookie 头(服务端渲染场景)
# 响应头示例:
# Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600
// 登录成功后轮换 session id(防会话固定)
app.post('/login', async (req, res) => {
const user = await verifyCredentials(req.body);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
// 轮换 session:销毁旧 session,创建新 session
const oldData = req.session;
req.session.regenerate((err) => {
if (err) return next(err);
req.session.userId = user.id;
req.session.csrfToken = generateCsrfToken();
res.json({ ok: true });
});
});
HTTP 方法与 OAuth
GET 不得有副作用;重要操作使用 POST、PUT、PATCH、DELETE。避免「为方便分享而把删除做成 GET」类路由反模式。
OAuth / OIDC 授权请求与回调使用 state(及 PKCE 的 code_verifier)防 CSRF;SKILL 审查是否省略 state 或固定弱值。
嵌入、客户端面与测试
跨站嵌入(iframe)注意顶层导航、Clickjacking(Content-Security-Policy: frame-ancestors 等)与 Cookie 策略联动。
- API:区分浏览器与原生客户端的 CSRF 暴露面;移动端 WebView 按同源与 Cookie 行为单独测。
- 测试:自动化跨站表单提交、预检失败与 token 缺失用例;与安全扫描中的「CSRF」规则对齐误报说明。
SKILL 片段
---
name: csrf-protection-review
description: 实现或审查 CSRF Token、SameSite Cookie、Double Submit 模式与框架配置
---
# 步骤
1. 确认应用形态:SSR表单+Cookie / SPA+Cookie / SPA+Bearer / 原生客户端
2. SSR 表单:实现同步 CSRF token(generateCsrfToken + timingSafeEqual 校验)
3. 登录后:session.regenerate() 轮换 session id,同时刷新 CSRF token
4. Cookie 配置:httpOnly=true, secure=true, sameSite='Lax'(默认)或 'Strict'
5. 跨站嵌入:sameSite='None'; secure=true + 收紧 CORS + CSRF token 双重防护
6. SPA + Cookie:X-CSRF-Token 自定义头 + CORS 白名单(禁止反射 Origin)
7. Double Submit Cookie:客户端从 cookie 读取 token 放入 header;服务端比较两者
8. Express:csrf 中间件或自实现 + timingSafeEqual 常量时间比较
9. Django:CsrfViewMiddleware + CSRF_TRUSTED_ORIGINS + CSRF_COOKIE_SAMESITE
10. Spring:CookieCsrfTokenRepository.withHttpOnlyFalse() + newSession() 固定防护
11. OAuth/OIDC:state 参数随机且会话绑定;公共客户端使用 PKCE
12. GET 不改状态:路由层面禁止 GET 有副作用
# 禁止
- 禁止 GET/HEAD 改变服务端状态
- 禁止 CSRF token 用固定值或可预测值
- 禁止跳过 timingSafeEqual(防时序旁路攻击)
审查面清单(csrf-page JS)
勾选场景后生成本页专用的审查备忘(仅本地、无网络);贴进 issue 或 SKILL 自检列表前请按实际栈裁剪。
审查备忘