攻击原理
CSRF(Cross-Site Request Forgery,跨站请求伪造):攻击者诱导受害者的浏览器,在受害者已登录的情况下,向目标网站发送一个非本意的请求。
关键点:浏览器会自动在请求中带上目标网站的 Cookie(包括登录凭证)。
例子
- 用户登录了银行网站
bank.com,Cookie 有效; - 用户访问了攻击者的页面
evil.com; - 这个页面包含:
<img src="https://bank.com/transfer?to=attacker&amount=10000">或者一个自动提交的表单。
- 浏览器加载这个"图片"时,会自动带上 bank.com 的 Cookie;
- 银行服务器收到一个带有有效登录凭证的转账请求——看起来完全合法;
- 钱被转走了。
这就是 CSRF 的可怕之处:攻击者不需要窃取 Cookie,只需要"借用"用户的登录状态。
为什么能成功
Cookie 的自动发送机制是 Web 的基础功能——但也因此成为攻击面。
浏览器对跨站请求的行为:
<img>、<script>、<form>发起的跨站请求会带上 Cookie(传统行为);- 而
fetch/XHR 的跨站请求受 CORS 限制(前文提到过)。
注意:CORS 阻止的是读取响应,不是阻止发送请求。因此仅靠 CORS 不能防御 CSRF。
防御
一、SameSite Cookie(现代首选)
在设置 Cookie 时指定 SameSite 属性:
Set-Cookie: session=xxx; SameSite=Lax; Secure; HttpOnly- Strict:完全不发送跨站 Cookie。最安全,但会影响正常的跨站链接跳转体验(从外部链接进入时未登录);
- Lax(默认值,现代浏览器):允许在顶层导航的 GET 请求中发送 Cookie(如点击链接跳转),但不允许在跨站的
POST、<img>、<iframe>、XHR 中发送。这阻挡了绝大多数 CSRF; - None:总是发送——需要配合
Secure(HTTPS)。用于需要跨站的场景(如嵌入的第三方内容)。
SameSite=Lax 是目前最实用的默认防御,现代浏览器默认采用它。
二、CSRF Token(经典方案)
服务端生成一个随机的、与用户会话绑定的 token,嵌入到表单或页面中。
提交请求时必须带上这个 token——服务器验证它。
攻击者无法获得这个 token(因为无法读取目标网站的响应内容——CORS 阻止了这一点),因此伪造的请求会缺少有效 token。
实现方式:
- 同步器令牌模式(Synchronizer Token):token 存在服务端会话中,每次渲染表单时插入隐藏字段;
- 双重提交 Cookie(Double Submit Cookie):token 同时写入 Cookie 和表单字段,服务器比对两者是否一致(不需要服务端存储,便于无状态架构)。
要求:token 必须不可预测、与用户绑定、只在敏感操作中验证。
三、验证 Origin / Referer 头
检查请求的 Origin 或 Referer 头,确认请求来自本站。
局限:
- 某些情况下这些头可能被省略(如从 HTTPS 跳到 HTTP);
- 依赖浏览器的正确实现;
- 因此通常作为辅助手段,而非唯一防御。
四、要求用户二次确认
对敏感操作(转账、修改密码、删除数据):
- 要求重新输入密码;
- 或要求输入手机/邮箱验证码;
- 或要求完成一次 MFA。
这是最可靠的——因为它让"非本意的请求"无法完成。
推荐在关键操作上使用(即使有其他 CSRF 防护)。
五、关键操作用非 GET 方法
CSRF 最容易通过 <img>、链接等 GET 请求实现。
因此:任何修改状态的操作都不要用 GET(这也是 HTTP 语义的要求——GET 应该是安全的、幂等的)。
一个常见误解
"用了 HTTPS 就不会有 CSRF。"
不对。 HTTPS 防止的是通信被窃听和篡改,不防止浏览器自动发送 Cookie。
同样,用了 JWT 也不能免疫——如果 JWT 存在 Cookie 中,依然会被自动发送。
(如果 JWT 存在 localStorage 中、且通过 JS 手动添加到请求头,则不会自动发送——但那会带来 XSS 风险,需要权衡。)
一句话
现代 Web 应用应该:
- 默认使用
SameSite=Lax(或 Strict)Cookie; - 敏感操作额外验证(CSRF token 或二次确认);
- 状态修改操作不使用 GET。
说明:本文为 Web 安全的知识整理。