CSRF 令牌防护:同步器 Token 与 SameSite
什么是 CSRF 攻击
跨站请求伪造(CSRF,Cross-Site Request Forgery)是一种网络攻击。它诱导登录用户在不知情的情况下,向受信任的网站发送非本意的请求。攻击者利用浏览器自动携带身份凭证(如 Cookie)的特性,在第三方站点伪造请求。
典型攻击流程
- 用户登录银行网站
bank.com,浏览器保存会话 Cookie。 - 用户访问恶意网站
evil.com,其中包含隐藏表单或自动提交的图片:<img src="https://bank.com/transfer?amount=1000&to=attacker"> - 浏览器向
bank.com发起 GET 请求,自动携带 Cookie。 - 银行服务器验证 Cookie 后执行转账操作,用户钱财被盗。
核心弱点
- 浏览器自动附带 Cookie 的机制。
- Web 应用仅依靠 Cookie 辨别用户身份,未验证请求来源的意图。
防御原理:引入 CSRF 令牌
防御 CSRF 的关键是在每个状态变更请求中嵌入一个无法被攻击者获取的秘密值。CSRF 令牌便是这样的秘密。它由服务器生成并与用户会话关联,要求客户端在提交请求时一并发送。由于同源策略限制,攻击者无法读取跨域返回的令牌,因此无法构造合法请求。
同步器 Token 模式(Synchronizer Token Pattern)
同步器 Token 模式是最经典、最可靠的 CSRF 防御方案之一。它将令牌存储于服务器端会话中,要求每个请求携带的令牌与服务器存储的严格匹配。
工作流程
1. 生成与注入
用户登录后,服务器生成一个随机、不可猜测的令牌(例如 128 位随机字符串),存入服务器端 Session:
Session["csrfToken"] = crypto.randomBytes(16).toString('hex');
同时将令牌嵌入前端页面。通常通过以下方式之一传递:
- 隐藏表单域:
<input type="hidden" name="csrf_token" value="..."> - 请求头:
X-CSRF-Token(常用于 SPA) - 元标签:
<meta name="csrf-token" content="...">(由 JavaScript 读取后加入请求头)
2. 客户端携带
表单提交时,令牌随表单数据发送;AJAX 请求则通过自定义请求头携带。例如:
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify(data)
});
3. 服务器验证
服务器接收到请求后,提取请求中的令牌,与 Session 中存储的令牌进行比较:
- 若两者一致,处理请求并生成新令牌(可选,用于防范重放攻击)。
- 若缺失或不一致,拒绝请求(通常返回 403 Forbidden)。
关键实现细节
- 令牌生成要求:必须使用密码学安全的伪随机数生成器(CSPRNG)。避免使用时间戳或简单递增序列。
- 绑定会话:令牌与当前用户会话严格绑定,不可跨用户共享。
- 传输安全:整个通信必须使用 HTTPS,防止中间人截获令牌。
- 双令牌防御:一些框架生成一对令牌:一个保存在 Cookie(仅由服务器设置,
HttpOnly非必须),一个放在页面。客户端携带的令牌与 Cookie 中的进行比较,服务器只需验证两者是否匹配,无需在服务器端存储状态。这被称为 Double Submit Cookie 模式,但必须配合加密签名或 HMAC 防止令牌被篡改,否则攻击者可通过 Cookie 注入绕过保护。
SameSite Cookie 属性:浏览器的原生防线
SameSite 是一种 Cookie 属性,用于声明 Cookie 仅在特定跨站场景下发送。它从浏览器层面大幅减少了 CSRF 攻击面。
SameSite 取值与行为
| 取值 | 跨站请求是否携带 Cookie | 典型场景 |
|---|---|---|
| Strict | 仅当请求完全来自同一站点时携带。例如从 site.com 的页面点击链接访问 site.com。 |
适合高安全性操作,但对用户从外部链接首次访问不友好。 |
| Lax(多数浏览器默认值) | 在顶级导航的 GET 请求中允许携带(如点击链接),阻止子资源加载(img、iframe)、AJAX 和 POST 表单提交携带。 | 平衡安全与体验,适合会话 Cookie。 |
| None | 总是携带,必须同时设置 Secure 属性(仅 HTTPS 下可用)。 |
需要跨站访问的场景(如嵌入式第三方组件)。 |
设置方式
HTTP 响应头:
Set-Cookie: sessionid=abc123; SameSite=Lax; Secure; HttpOnly
JavaScript document.cookie 无法设置 HttpOnly 属性的 Cookie,但可设置 SameSite(Secure 需确保页面为 HTTPS)。
SameSite 如何防御 CSRF
以 SameSite=Lax 为例:
- 当用户从
evil.com通过<img>、<form method="POST">或 AJAX 向bank.com发起请求时,浏览器不会附加 Cookie。 - 攻击者无法利用用户登录态执行敏感操作。
- 正常站内导航和表单提交不受影响(同站请求依然携带 Cookie)。
局限性
- 旧版浏览器不支持或默认行为有差异。
SameSite=Lax不能防御通过顶级导航 GET 请求发起的 CSRF(如<a> href修改邮箱的链接)。应始终遵循“GET 请求仅做只读查询”的设计。- 对于需要跨站发送请求的应用(如 OAuth 回调),不能依赖 SameSite 完全解决 CSRF,必须配合令牌。
组合防御:同步器 Token + SameSite
将 CSRF 令牌与 SameSite 结合,可以构成纵深防御:
-
SameSite 作为第一层过滤
对现代浏览器,SameSite=Lax直接阻断大多数跨站 POST 请求携带会话 Cookie,极大降低令牌被攻击者利用的机会。 -
CSRF 令牌作为第二层验证
对于:- 不支持 SameSite 的浏览器
- 必须使用
SameSite=None的跨站场景 - 通过 GET 请求进行状态变更的风险(应禁止)
CSRF 令牌提供可靠的状态变更保护。即使 Cookie 被意外携带,没有合法令牌的请求仍被拒绝。
-
实施建议
- 对所有状态变更接口(POST、PUT、DELETE 等)强制验证 CSRF 令牌。
- 设置会话 Cookie 为
SameSite=Lax(或Strict,根据用户体验需求),并附上Secure和HttpOnly。 - 避免将令牌放入 URL 或 GET 请求参数,防止被 Referer 头泄露或浏览器历史记录保存。
- 定期轮换令牌或每次使用后更新,降低令牌泄露风险。
示例架构
- 身份认证后,服务器设置 Session Cookie:
Set-Cookie: SESSIONID=abc; SameSite=Lax; Secure; HttpOnly; Path=/ - 返回的 HTML 中嵌入隐藏令牌:
<input type="hidden" name="_csrf" value="8fK9erRq..."> - 所有 AJAX 读取
<meta name="csrf-token">并在请求头中发送。 - 服务器拦截器检查令牌有效性,若不匹配则返回 403。
常见误区与注意事项
-
仅依赖 Referer/Origin 校验
头字段可能被用户代理、隐私设置或网络环境修改或缺失,只能作为辅助手段,不可替代令牌。 -
在 GET 请求中使用 CSRF 令牌
令牌暴露在 URL 中,易被 Referer 泄露、浏览器历史记录、日志等泄露,违背令牌机密性。GET 应只用于无害读取。 -
复用相同令牌或弱算法
令牌若可预测(如时间戳哈希),攻击者可尝试猜测。必须使用足够熵值的随机值。 -
忽视子域安全
如果sub.evil.com能够设置针对父域的 Cookie,可能利用 Cookie 写入绕过 SameSite 或双重提交令牌。应严格限制 Cookie 的作用域,设置Domain和Path为最精确值。 -
认为 SameSite 可以完全取代 CSRF 令牌
SameSite 是浏览器端的一种增强限制,不可作为唯一防御。务必结合令牌或其他确认请求意图的机制。
总结
- CSRF 令牌(同步器 Token 模式)是经过实战检验的服务端状态验证方案,能确保请求源于用户真实意图。
- SameSite Cookie 在现代浏览器中提供轻量级的跨站请求防护,尤其适用于阻止非导航型恶意请求。
- 安全最佳实践:令牌 + SameSite + HTTPS + 正确 HTTP 方法语义 形成多层保护。
- 投入实战时,首选成熟框架的内置 CSRF 防护(如 Spring Security、Django、Laravel、Ruby on Rails),它们已正确实现上述机制,避免重复造轮带来的安全漏洞。