🛡️ 第四章 系统漏洞的防范
📄 一句话概括

跨网站请求伪造(CSRF)不是漏洞,而是浏览器一个"贴心功能"的副作用:浏览器在你访问某个网站时会自动把这个网站的 Cookie 一起带上,而它分不清"这个请求是你自己点的"还是"别人引着你的浏览器发的"——于是攻击者只要让你打开一个页面,就能让你的浏览器以你的身份,向另一个网站提交一个你并不想提交的请求。

📝 本节和前面几节的关系

4-5 讲的是 XSS:攻击者把脚本放进了你的页面里,他拿到的是"在你的页面上执行代码"的能力。这一节讲的是 CSRF:攻击者没有往你的页面里放任何东西,他只是让你"顺手"发了一个请求——两节合起来是一对:XSS 是"让你的页面替他说",CSRF 是"借你的手替他做"。

两者经常被一起讨论,是因为它们有一个共同的支点:Cookie(尤其是会话 Cookie)。 XSS 想偷它,CSRF 想借它。而它们的解法完全相反:XSS 防的是"输出",CSRF 防的是"来源"。 下一节 4-7 会把 Cookie 上那几个属性一次讲清。

一、先记住一句话

CSRF 之所以成立,靠的是一个正常且必要的行为:

浏览器在你访问 A 网站时,会自动把 A 网站的 Cookie 一起发过去。

这个行为的本意是"方便"——你不用每点一个链接都重新登录一次。但它带来一个副作用:只要有请求发往 A 网站,浏览器就会自动附上你的身份凭证,而它并不检查"你这个请求是在哪里被发起的"。

所以 CSRF 的完整链条是:

  1. 你登录了 A 网站(浏览器里有了 A 的会话 Cookie);
  2. 你在另一个标签页打开了一个攻击者的页面;
  3. 那个页面里藏着一个"发往 A 网站"的请求(可能只是一个 <img> 标签的地址,或者一个自动提交的表单);
  4. 你的浏览器发这个请求时,自动带上了 A 的会话 Cookie;
  5. 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。

读这个输出:

四、⚠️ 2026 现状

30 秒实验

今天做两个动作,各 15 秒:

  1. 在浏览器里打开你常用的一个网站,按 F12 → Application(应用)→ Cookies,看会话 Cookie 的 SameSite 那一列。如果写的是 Lax 或 Strict,说明这个站点已经考虑了 CSRF;如果写的是 None,看它有没有配套的 Token 机制(没有的话,说明它把银行这类敏感操作暴露在了 CSRF 面前)。这一步能让你亲眼看到"浏览器默认值"在真实站点上的样子。
  2. 想一下你自己维护的东西里,有没有"用 GET 实现状态变更"的地方。判断标准很简单:把那个功能的链接复制到浏览器地址栏直接回车,如果能生效(比如删除成功、设置被改),那它就是一个 CSRF 入口。正确做法是改成 POST,并加上 Token——这是这一节里最容易落地、收益又最直接的一个动作。

这一节要带走的一句是:CSRF 伪造的不是你的密码,而是"你的意图"——所以防它的关键不是"验证你是谁"(Cookie 已经证明了你是你),而是"验证这个请求是你想发的"。

常见误区

  1. 错误:CSRF 需要攻击者拿到我的 Cookie。——纠正:恰恰相反,CSRF 的成立完全不需要攻击者知道你的 Cookie。他要的只是"你的浏览器替你自动带上 Cookie"这个行为——整个过程里,他的页面甚至读不到 Cookie 的内容(那是别的域名的 Cookie)。 这正是 CSRF 和 XSS 的关键区别: XSS 是"把 Cookie 偷走、拿到攻击者自己的机器上去用";CSRF 是"根本不用偷,直接借用你浏览器这个自动带 Cookie 的通道"。 所以"Cookie 设了 HttpOnly"对 CSRF 一点帮助都没有(HttpOnly 防的是 JS 读取,而 CSRF 不需要读取)。
  2. 错误:CSRF 只影响老网站,现代框架都自带防护。——纠正:框架的防护往往需要"正确启用"。比如"自动在表单里插入 Token"这一类功能,如果接口是用前端自己拼的 fetch 调用、或者用了不经过框架模板的路径,Token 就可能没被带上;而 SameSite 的默认值也会被一些"为了跨站能用"的需求主动关掉。证据是:2026 年的插件统计里,与请求校验、授权校验相关的问题仍然占了很高比例——这些代码大多运行在现代语言和框架上。 所以正确的说法是:框架降低了出错的概率,但没有取消"知道自己在防什么"的责任。
  3. 错误:同一个浏览器的两个标签页之间不会有 CSRF 问题。——纠正:CSRF 的"跨站"指的是"请求发起的来源页面"和"请求要去的站点"不是同一个,而不是"两个标签页"。所以你在 A 网站登录、然后在 B 网站(攻击者的站)打开一个页面,B 里的请求发往 A——这就是标准的 CSRF,和你用几个标签页无关。 反过来说,一个请求是不是 CSRF,看的是"发起页面的来源(Origin)"是不是目标站点自己——这也是"Origin 校验"能作为一层防御的原因。
  4. 错误:加了一个验证码,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 里"默认安全"的理念)。

相关笔记