跨网站请求伪造(CSRF)不是漏洞,而是浏览器一个"贴心功能"的副作用:浏览器在你访问某个网站时会自动把这个网站的 Cookie 一起带上,而它分不清"这个请求是你自己点的"还是"别人引着你的浏览器发的"——于是攻击者只要让你打开一个页面,就能让你的浏览器以你的身份,向另一个网站提交一个你并不想提交的请求。
4-5 讲的是 XSS:攻击者把脚本放进了你的页面里,他拿到的是"在你的页面上执行代码"的能力。这一节讲的是 CSRF:攻击者没有往你的页面里放任何东西,他只是让你"顺手"发了一个请求——两节合起来是一对:XSS 是"让你的页面替他说",CSRF 是"借你的手替他做"。
两者经常被一起讨论,是因为它们有一个共同的支点:Cookie(尤其是会话 Cookie)。 XSS 想偷它,CSRF 想借它。而它们的解法完全相反:XSS 防的是"输出",CSRF 防的是"来源"。 下一节 4-7 会把 Cookie 上那几个属性一次讲清。
一、先记住一句话
CSRF 之所以成立,靠的是一个正常且必要的行为:
浏览器在你访问 A 网站时,会自动把 A 网站的 Cookie 一起发过去。
这个行为的本意是"方便"——你不用每点一个链接都重新登录一次。但它带来一个副作用:只要有请求发往 A 网站,浏览器就会自动附上你的身份凭证,而它并不检查"你这个请求是在哪里被发起的"。
所以 CSRF 的完整链条是:
- 你登录了 A 网站(浏览器里有了 A 的会话 Cookie);
- 你在另一个标签页打开了一个攻击者的页面;
- 那个页面里藏着一个"发往 A 网站"的请求(可能只是一个
<img>标签的地址,或者一个自动提交的表单); - 你的浏览器发这个请求时,自动带上了 A 的会话 Cookie;
- A 网站看到的是一个"带着合法凭证的正常请求",于是照做了。
注意第 5 步:A 网站并没有出错——从它的角度看,这个请求和你在 A 网站上亲手点的那一下完全一样。
这就是 CSRF 难防的地方:它伪造的不是凭证,而是"意图"。
二、一个请求"算不算 CSRF",看四件事
要判断一个请求会不会被 CSRF 利用,只需要看四个条件。这四个条件也是后面所有防御手段的入手处:
| 条件 | 含义 | 防御对应 |
|---|---|---|
| 是否跨站 | 请求是从别的网站发起的吗 | 同源策略、Origin 校验 |
| 方法 | 是 GET 还是 POST | 不该用 GET 做状态变更 |
| Cookie 的 SameSite | 跨站时带不带 Cookie | SameSite=Lax/Strict |
| 有没有额外的校验 | 有没有 Token、验证码 | CSRF Token(双提交 Cookie) |
这里最容易被忽略、也最值得记住的一点是 Cookie 的 SameSite 属性:它专门用来回答"跨站的请求,要不要带上这个 Cookie"。 现代浏览器对"没写 SameSite 的 Cookie"默认按 Lax 处理——这一个默认值,就消掉了最简单的那一类 CSRF。
三、动手 Demo:8 个场景,谁被拦下
下面这段脚本用一个判定函数,模拟"浏览器 + 站点"在不同配置下会怎么处理同一个请求:
def judge(method, cross, ss, token, nav):
if not cross:
return "放行", "同站请求,Cookie 正常"
if ss == "Strict":
return "拦截", "Strict 下跨站一律不带 Cookie"
if method in ("GET", "HEAD") and nav:
return "放行", "顶层导航,Lax 会带 Cookie"
if ss == "Lax":
return "拦截", "Lax 不带跨站子资源的 Cookie"
if token:
return "放行", "有会话且 Token 校验通过"
return "拦截", "有会话但缺 Token,返回 403"
# 参数依次为:(方法, 是否跨站, SameSite, 有没有 Token, 是否为顶层导航)
实跑输出:
先记住一句话:CSRF 不是漏洞本身,而是【浏览器自动带 Cookie】
这个特性的副作用 —— 浏览器分不清这个请求是你点的还是别人引的。
场景 方法 跨站 SameSite Token 判定 理由
----------------------------------------------------------------------------------------------
同站表单提交 POST 否 Lax 有 放行 同站请求,Cookie 正常
跨站自动提交表单 POST 是 Lax 无 拦截 Lax 不带跨站子资源的 Cookie
跨站点开链接 GET 是 Lax 无 放行 顶层导航,Lax 会带 Cookie
跨站点开链接(Strict) GET 是 Strict 无 拦截 Strict 下跨站一律不带 Cookie
跨站 fetch(None,无 Token) POST 是 None 无 拦截 有会话但缺 Token,返回 403
跨站 fetch(None,有 Token) POST 是 None 有 放行 有会话且 Token 校验通过
跨站图片标签 GET 是 Lax 无 拦截 Lax 不带跨站子资源的 Cookie
同站 API 调用 POST 否 None 无 放行 同站请求,Cookie 正常
→ 8 个场景里被拦下 4 个。注意第 7 行:跨站的图片标签虽然只是
一次 GET,但它属于【子资源请求】,Lax 同样不带 Cookie。
→ 现代浏览器把没写 SameSite 的 Cookie 默认当 Lax,
这一个默认值就消掉了最简单的那一类 CSRF。
读这个输出:
- 第 3 行和第 7 行的对比是这张表的精华:两次都是"跨站 + GET + 无 Token",一个放行、一个拦截。区别在于"请求的类型":第 3 行是你在地址栏敲进去(或点一个链接)的"顶层导航"——这是用户明确要访问那个网站,Lax 会带 Cookie;第 7 行是页面里的一个
<img>标签——这是"子资源请求",浏览器认为它不该带 Cookie,于是拦下。 所以"Lax"这个名字的含义就是:只对"用户主动点开的顶级页面"放行 Cookie,其余跨站请求都不带。 - 第 4 行说明了 Strict 的代价:
Strict更安全,但会让"从别的网站点链接过来"变成一次未登录状态——用户点开微信里分享的链接,进到网站发现是"未登录",就是这个原因。 所以 Strict 一般只用在真正敏感的 Cookie 上。 - 第 5 行和第 6 行的对比说明了 Token 的作用:同样跨站、同样
SameSite=None(也就是明确允许跨站带 Cookie),有 Token 就放行、没 Token 就 403。 这说明 SameSite 和 Token 是两条独立的防线——SameSite 是"浏览器帮你拦",Token 是"站点自己校验"。在需要跨站带 Cookie 的场景(比如嵌入式的登录、跨域的 API),SameSite 这条线没法用,就只剩 Token 这条路。
四、⚠️ 2026 现状
- 【SameSite 的默认值,已经替所有人挡掉了最简单的那一类 CSRF】。 现代浏览器对"没有显式声明 SameSite 的 Cookie"默认按
Lax处理,这一个改动让"靠一个<img>标签或自动提交表单完成攻击"这种最经典的 CSRF 写法基本失效。所以今天再谈 CSRF,重点已经不是"有没有 SameSite"(默认就有了),而是"这条防线在你需要的场景里够不够用"。 - 【三层防御,缺一层就有缝】。 现在通行的做法是同时叠三层:① Cookie 设 SameSite(默认 Lax,敏感操作上 Strict);② 状态变更的请求加 CSRF Token(或双提交 Cookie);③ 服务端校验
Origin/Referer请求头。为什么要叠三层?因为每一层都有绕过的可能:SameSite 可以因为老浏览器或跨站需要而被关掉;Token 可以因为实现不当被泄露;Origin 头在某些旧客户端里可能不带——三层里只要有一层能兜住,攻击就不成立。 - 【一个更根本的原则:不要用 GET 做状态变更】。 GET 请求在设计上应该是"只读"的——这也正是"跨站图片标签"能成为攻击载具的原因。所以"用 GET 实现登出、删除、修改"这些接口,等于把 CSRF 的攻击面主动放大。2026 年的各类指南(包括 4-14 提到的协调披露指南)在讲安全编程时,都会把"方法语义要和动作性质一致"列为基本要求。
- 【CSRF 的现身高发地:插件、老旧管理面板、IoT 的网页界面】。 这三类地方的共同点是"代码写得早、或者维护力度弱":2026 年的插件生态统计里,WordPress 插件单周 358 条漏洞中,与"授权校验、请求校验"相关的问题占了很大比重(4-12)。很多家用路由器、摄像头、NAS 的管理页面也属于这一类——它们的接口经常用 GET 完成设置变更,且没有 Token。这也是 3-10 讲过的"物联网设备默认配置薄弱"的又一个表现。
- 【和 XSS 的关系:不要让 XSS 把你的 CSRF 防线一起破掉】。 如果站点里存在 XSS,那么攻击者可以在你的页面上读取 Token 并带着它发请求——这时 Token 这一层就失效了。 所以这两节要连着看:CSRF 的防御(Token)建立在"页面本身没有被注入"的前提上,而这个前提由 XSS 的防御(输出转义)提供。
30 秒实验
今天做两个动作,各 15 秒:
- 在浏览器里打开你常用的一个网站,按 F12 → Application(应用)→ Cookies,看会话 Cookie 的 SameSite 那一列。如果写的是
Lax或Strict,说明这个站点已经考虑了 CSRF;如果写的是None,看它有没有配套的 Token 机制(没有的话,说明它把银行这类敏感操作暴露在了 CSRF 面前)。这一步能让你亲眼看到"浏览器默认值"在真实站点上的样子。 - 想一下你自己维护的东西里,有没有"用 GET 实现状态变更"的地方。判断标准很简单:把那个功能的链接复制到浏览器地址栏直接回车,如果能生效(比如删除成功、设置被改),那它就是一个 CSRF 入口。正确做法是改成 POST,并加上 Token——这是这一节里最容易落地、收益又最直接的一个动作。
这一节要带走的一句是:CSRF 伪造的不是你的密码,而是"你的意图"——所以防它的关键不是"验证你是谁"(Cookie 已经证明了你是你),而是"验证这个请求是你想发的"。
常见误区
- 错误:CSRF 需要攻击者拿到我的 Cookie。——纠正:恰恰相反,CSRF 的成立完全不需要攻击者知道你的 Cookie。他要的只是"你的浏览器替你自动带上 Cookie"这个行为——整个过程里,他的页面甚至读不到 Cookie 的内容(那是别的域名的 Cookie)。 这正是 CSRF 和 XSS 的关键区别: XSS 是"把 Cookie 偷走、拿到攻击者自己的机器上去用";CSRF 是"根本不用偷,直接借用你浏览器这个自动带 Cookie 的通道"。 所以"Cookie 设了 HttpOnly"对 CSRF 一点帮助都没有(HttpOnly 防的是 JS 读取,而 CSRF 不需要读取)。
- 错误:CSRF 只影响老网站,现代框架都自带防护。——纠正:框架的防护往往需要"正确启用"。比如"自动在表单里插入 Token"这一类功能,如果接口是用前端自己拼的 fetch 调用、或者用了不经过框架模板的路径,Token 就可能没被带上;而 SameSite 的默认值也会被一些"为了跨站能用"的需求主动关掉。证据是:2026 年的插件统计里,与请求校验、授权校验相关的问题仍然占了很高比例——这些代码大多运行在现代语言和框架上。 所以正确的说法是:框架降低了出错的概率,但没有取消"知道自己在防什么"的责任。
- 错误:同一个浏览器的两个标签页之间不会有 CSRF 问题。——纠正:CSRF 的"跨站"指的是"请求发起的来源页面"和"请求要去的站点"不是同一个,而不是"两个标签页"。所以你在 A 网站登录、然后在 B 网站(攻击者的站)打开一个页面,B 里的请求发往 A——这就是标准的 CSRF,和你用几个标签页无关。 反过来说,一个请求是不是 CSRF,看的是"发起页面的来源(Origin)"是不是目标站点自己——这也是"Origin 校验"能作为一层防御的原因。
- 错误:加了一个验证码,CSRF 就彻底解决了。——纠正:验证码确实能挡住"自动提交"这一类 CSRF,但它代价很大(每个操作都要人手动输一次,会明显影响正常使用),所以它一般只用在高危操作上(比如改密码、转账)。而且它挡不住"诱导用户自己去点"这类攻击——如果攻击者能在页面上做一个"你自己会想点"的按钮,验证码也拦不住。 更根本的问题是:验证码是"增加攻击成本",而 Token 是"从结构上让跨站请求不带凭证"——能用后者解决的地方,不该用前者。
自测三问(点开对答案)
1. CSRF 里攻击者"伪造"的到底是什么? —— **伪造的是"意图",不是"身份"**。**你的 Cookie 是真的,你的会话是真的,请求也是你的浏览器真的发出去的**——**唯一不真实的是"你想不想发这个请求"**。**这就是为什么 CSRF 那么难防**:**所有"验证你是谁"的手段(密码、Cookie、甚至多因子认证)都过不了这一关,因为"你"确实就是你。** **所以 CSRF 的防御必须回答另一个问题:** **"这个请求是从哪里发起的?"**(→ Origin 校验、SameSite)**"发起的时候还带了什么只有真页面才有的东西?"**(→ CSRF Token)。 2. 为什么"Cookie 设了 HttpOnly"对 CSRF 毫无帮助? —— **因为 HttpOnly 防的是"JavaScript 读取 Cookie",而 CSRF 根本不需要读**。**CSRF 利用的是"浏览器自动把 Cookie 附加到请求上"这个行为**——**攻击者的页面从来不需要知道 Cookie 里是什么,它只需要让浏览器发一个请求,Cookie 就会自己跟着去。** **这也是为什么这两条防线必须分开设:** **HttpOnly 保护的是"令牌本身不被偷"(防的是 XSS 的后果之一),SameSite 保护的是"令牌不会被跨站请求顺带使用"(防的是 CSRF)**。**它们各自挡住一类攻击,但不能互相替代**(下一节 4-7 会把这个"互不替代"讲得更细)。 3. 为什么 Modern 浏览器把 SameSite 默认设成 Lax,是一个"性价比很高"的安全改进? —— **因为它几乎不付出任何代价,就消掉了最常见的一整类 CSRF**。**Lax 的规则是:只对"用户主动点开链接/在地址栏输入"的顶层导航带上 Cookie,对图片、脚本、iframe、跨站表单提交这些"子资源请求"一律不带**。**这一条规则恰好覆盖了最经典的 CSRF 载具(跨站图片标签、跨站自动提交表单),而几乎不影响正常使用**——**用户平时点链接进来、正常登录浏览,都还是带着 Cookie 的。** **相比之下,`Strict` 虽然更安全,但会让"从别的网站点链接过来"变成未登录状态**,**体验代价明显**。**所以这个默认值的效果是:在不打扰用户的前提下,把攻击成本抬高了一大截**——**这也是"用默认值做安全"这个思路最成功的例子之一**(呼应 4-11 里"默认安全"的理念)。- ☐ 下一节(4-7 用户登录状态的劫持)
相关笔记
- 跨越多个网站的攻击 —— 上一篇:XSS 是"替他说",CSRF 是"借你的手做"
- 用户登录状态的劫持 —— 下一篇:Cookie 上的四个属性各挡什么
- 计算机病毒感染的不仅仅是计算机 —— 物联网设备的网页界面是 CSRF 高发地
- 当心那些使用方便的工具中存在的漏洞 —— 插件里请求校验类问题占比很高
- 🛡️ 网络与信息安全 · 索引 —— 全课路线图