CSRF 跨站请求伪造防护:令牌、SameSite 与校验

FreeGuideOnline 最新 2026-06-13

CSRF 是什么?为什么危险?

CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种攻击方式,攻击者诱导用户点击链接或访问恶意页面,利用用户已登录的凭证(通常是 Cookie)以用户的名义向目标网站发起非预期的请求。这让攻击者能够在用户无感知的情况下,执行转账、修改资料、发帖等操作。

简单理解:你登录了银行网站,然后打开了一个恶意网页,恶意网页里藏了一段自动提交的代码,向银行发起转账请求。因为浏览器会自动携带银行站点的 Cookie,所以银行以为是你本人操作。

常见攻击场景

  • 用户登录 bank.com,浏览器存有会话 Cookie。
  • 用户访问恶意站点 evil.com,该页面包含隐藏的表单或自动提交的 <img> 标签:
    <form action="https://bank.com/transfer" method="POST">
      <input type="hidden" name="to" value="attacker" />
      <input type="hidden" name="amount" value="10000" />
    </form>
    <script>document.forms[0].submit();</script>
    
  • 浏览器携带 Cookie 发送请求,银行服务器接收到请求后验证 Cookie 有效,于是执行转账。

CSRF 攻击集中在状态改变的请求上(如 POST、PUT、DELETE),因为单纯读取数据的 GET 请求通常不会造成直接危害(但可能泄露隐私,现代浏览器已限制跨站点携带凭证的 GET 请求的副作用)。


防护基础:验证请求的来源与意图

防御 CSRF 的核心思路:让服务器能够精确区分一个请求是发自真实用户的自主操作,还是被恶意网站伪造的。实现方式主要包括三种主流手段,它们既相互独立又能组合使用:

  1. 同步令牌模式(Synchronizer Token Pattern)
  2. SameSite Cookie 属性
  3. 基于请求头的校验(Origin/Referer 头)

下面逐一深入讲解。


同步令牌模式(CSRF Token)

原理

服务器生成一个随机、不可预测的字符串(令牌),将其发放给客户端。客户端在发起状态变更的请求(如 POST)时,必须附加该令牌。服务器验证令牌的有效性,若缺失或无效则拒绝请求。

由于攻击者无法读取或猜测合法用户的令牌(同源策略阻止恶意网站从目标站点取得数据),所以伪造请求无法通过验证。

实现要点

1. 令牌生成与存储

  • 令牌必须具有足够熵值,使用密码学安全的随机数生成器(如 crypto.randomUUID()secrets.token_hex(32))。
  • 令牌可绑定到用户会话,也可单独存储在服务端(如 Redis),设置合理过期时间。
  • 绝不要将令牌存储在前端可被跨站读取的位置(如纯前端 Cookie、LocalStorage 且无保护);通常服务器将其注入 HTML(<meta> 标签、隐藏字段)或通过安全的 Set-Cookie 配合 JavaScript 读取(需注意配置)。

2. 令牌传递

前端在 AJAX 请求中,将令牌添加到请求头(如自定义 X-CSRF-Token)或请求体中作为参数。传统表单则放入隐藏输入字段。

示例(Express.js + JavaScript 客户端):

// 服务端注入令牌到 cookie(非 HttpOnly,便于 JS 读取)
res.cookie('csrf-token', token, { sameSite: 'strict', secure: true });

// 前端读取 cookie 并添加到请求头
async function securePost(url, data) {
  const token = document.cookie.match(/csrf-token=([^;]+)/)[1];
  await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': token
    },
    body: JSON.stringify(data)
  });
}

3. 服务端验证

服务器取出请求中的令牌,与当前会话关联的令牌进行比对。常见验证策略:

  • 直接比较字符串(恒定时间比较防止时序攻击)。
  • 如令牌存储于独立的 token 表中,验证后立即删除(一次性令牌)。
  • 若使用加密令牌,服务器解密验证签名,不需额外存储。

常见误区

  • 将 CSRF Token 存入 HttpOnly Cookie 并由服务器自动回读:这没有防御效果,因为浏览器会自动携带 Cookie,攻击者不需要读取它。
  • 令牌固定不变:应为每个会话定期更换或每次用完重新生成,防止令牌泄露后被长期利用。
  • 对于 GET 请求也校验 Token:不建议,因为 GET 应该具有幂等性且链接可分享,加 token 会破坏这些特性。确保只有状态变更方法要求 Token。

SameSite 是 Cookie 的一个属性,用来控制跨站点请求时是否发送 Cookie。它有三个值:

  • Strict:完全禁止跨站发送 Cookie。用户从外部链接点击进入站点时,初始请求也不会携带 Cookie(需要重新登录或通过其他机制适应)。
  • Lax:较宽松,允许在顶级导航(top-level navigation)的 GET 请求中发送 Cookie,例如用户从其他网站点击链接跳转时。禁止在跨站点的 POST、iframe、AJAX 等子资源请求中发送 Cookie。这是现代浏览器的默认值。
  • None:跨站情况下一律发送 Cookie,必须同时设置 Secure 属性(即要求 HTTPS)。

防御 CSRF 的原理

CSRF 攻击依赖浏览器在跨站请求中自动携带目标站点的 Cookie。设置 SameSite=LaxStrict 后,恶意站点发起的 POST 请求(如表单提交、ajax 请求)不会携带 Cookie,服务器因此无法获取用户的登录状态,请求自然被拒绝。

推荐设置

Set-Cookie: sessionid=abc123; Path=/; HttpOnly; Secure; SameSite=Lax

适用场景与限制

  • SameSite=Lax 平衡了安全与用户体验,适合大多数 Web 应用。用户从搜索引擎点击链接进入网站时依然会携带 Cookie,保持登录状态。
  • SameSite=Strict 适合安全要求极高的场景(如银行后台),但可能造成从外部链接进入时要求重新登录。
  • 老式浏览器不支持 SameSite 属性,因此 SameSite 不能作为唯一防御,仍建议配合令牌使用。

基于请求头的校验(Origin/Referer)

利用 Origin 和 Referer 头

浏览器在发送跨站 POST 请求时,会自动添加 Origin 头(部分旧浏览器用 Referer,但后者易被用户设置屏蔽)。服务器可以检查这两个请求头是否与目标站点一致:

  • Origin:包含请求的协议、域名和端口,很可靠。
  • Referer:包含完整的源 URL,可能因隐私策略被省略。

校验逻辑

  1. 对于状态改变的请求,检查 OriginReferer 是否存在。
  2. 比较它们是否等于预定义的合法源(如 https://example.com)。
  3. 若不匹配或无此头,则拒绝请求。

示例(Express 中间件):

function verifyOrigin(req, res, next) {
  const allowedOrigin = 'https://myapp.com';
  const origin = req.get('Origin');
  if (req.method !== 'GET' && (!origin || origin !== allowedOrigin)) {
    return res.status(403).json({ error: 'Invalid origin' });
  }
  next();
}

注意事项

  • 不能依赖独自验证:有些旧设备或隐私插件会禁止发送 Referer,导致正常用户被拦截。应作为额外验证层或结合其他方案。
  • 小心子域名:如果应用和恶意站点同属于一个父域(如 a.example.comb.example.com),Origin 检测可能会失效,需要白名单精确控制。
  • HTTPS 站点:浏览器通常不会在从 HTTPS 跳转到 HTTP 时发送 Referer,如果你的站点强制 HTTPS,这通常不是问题。

组合防御:纵深防护策略

没有单点方案可以百分百抵御所有形式的 CSRF。现代最佳实践是分层防御

层级 措施
浏览器层 所有 Cookie 设置 SameSite=LaxStrict,并标记 SecureHttpOnly
应用逻辑层 对所有状态改变请求强制验证 CSRF Token 或双重提交 Cookie 模式。
请求头验证 检查 Origin / Referer,作为最后防线,拦截无头或异常请求。
用户交互确认 对敏感操作(转账、删除账户)要求二次认证(如密码、验证码)。

如果项目难以在服务端存储 token,可用双重提交 Cookie 方案:

  • 服务器生成一个随机值,通过 Set-Cookie 发至客户端(但这次 Cookie 不设置 HttpOnly,以便 JavaScript 读取)。
  • 客户端无论是用表单还是 AJAX,都必须从 Cookie 中读取该值,并将其作为请求参数或请求头发送回去。
  • 服务器只需要比较 Cookie 里的值与请求中的值是否一致。

攻击者无法读取或设置来自目标域的 Cookie(同源策略),因此无法构建一致的请求。此模式无状态,横向扩展友好,但必须确保 Cookie 使用 SameSiteSecure 避免被中间人篡改。


开发框架中的现成方案

多数成熟的 Web 框架已内置 CSRF 防护,只需正确配置:

  • Express (Node.js):使用 csurflusca 中间件,可结合 csurf({ cookie: true }) 实现双重提交。
  • Django:自带 django.middleware.csrf.CsrfViewMiddleware,在模板中用 {% csrf_token %} 标签。
  • Laravel (PHP):由 VerifyCsrfToken 中间件自动保护,所有 POST 表单必须包含 @csrf 指令。
  • Spring Security (Java):启用 CSRF 保护后,默认期望 _csrf 令牌。
  • Sails.js / Next.js 等也有相应插件或内置 API。

使用框架时,务必阅读文档,避免因误关保护而暴露漏洞。


测试与排查

验证防御是否有效

  • 搭建一个简单的恶意页面,向目标站点的敏感端点发起跨站 POST 请求(不加 token),检查服务器是否以 403 或重定向拒绝。
  • 使用浏览器开发者工具,检查会话 Cookie 的 SameSite 值。
  • 使用 POST 工具(如 curl 或 Postman)模拟无 Origin 头、错误 Origin 的请求,确保被拦截。

常见错误排查

  1. AJAX 请求未附带 token:检查请求头是否正确(注意大小写),以及 JavaScript 是否能读取 token(Cookie 设置正确,非 HttpOnly 等)。
  2. Token 验证失败但页面显示正常:可能是 token 没有随页面的动态加载更新(SPA 页面切换时需重新获取 token)。
  3. SameSite 不生效:确认域名完全匹配,并检查 HTTPS 环境。SameSite=None 未加 Secure 将被浏览器拒绝。
  4. 子域名间 token 不共享:如果 token 绑定在特定域名下,跨子域请求需要传递 token,可通过 CORS 配置和共享 Cookie 域解决,但要谨慎放宽。

总结

CSRF 防护的核心是确保请求的主动性预期性——即请求确实是你自己发起的,而不是被第三方诱导。记住三个关键词:

  • 令牌:不可预测的随机值,要求同源发送。
  • SameSite:浏览器级别的 Cookie 发送策略限制。
  • 校验头:检查请求来源的 Origin/Referer。

将这三种技术叠加使用,并始终遵循“状态改变必须保护”的原则,就能将 CSRF 风险降到最低。若你正在构建一个现代 Web 应用,请从项目初期就引入 CSRF 防护,它和 XSS 防御、HTTPS 一样,属于 Web 安全基线的一部分。