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: BearerX-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 自检列表前请按实际栈裁剪。

主要形态
附加面

审查备忘


                

返回技能库 更多技能入口