用户什么都没做错。 Demo 里攻击者的页面一被打开,转账请求就出去了:状态 200,余额 1000 变 500。 同一段代码加上令牌校验,回 403,余额一动不动。差别只在「多问了一句」。
11.4.1 要偷会话 ID,11.4.2 要借会话 ID。 这一节连 ID 长什么样都不用知道——浏览器会自动把 ID 带上。
一、请求是浏览器替你发的
cookie 的发送规则只有一条:目标站对得上,就带上那个站的 cookie,不管这个请求是谁发起的。
这一条本来是 cookie 的设计目的——换页面还认得你——但它同时就是这类漏洞的根。
所以攻击者只要让受害者的浏览器向银行发一个请求就够了:页面里放一个隐藏表单,
action 指向 https://bank.example/transfer,再补一句脚本自动提交。
用户看到的只是「打开了一个页面」。
Demo 里就是这样:恶意页面一加载,转账请求就发出去了,状态 200,正文「已转 500 给 attacker,
余额 500」。用户没点任何按钮,没输任何密码。
服务端每一步都合规:cookie 有效、方法正确、参数合法。
它唯一没检查的是——这个请求是不是用户自己想发的。
二、令牌:藏在页面里的那句话
令牌是服务端渲染表单时随机生成的一串值:一份写进表单的隐藏字段,一份记在服务端的会话里。 提交时两边比对,不一样就拒。 关键不在随机,在攻击者读不到。他没法用脚本去读银行页面里的内容,同源策略拦着; 所以他可以伪造出一个结构完全正确的请求,却猜不出那串随机的值。 Demo 第二段:令牌对不上,回 403,正文「令牌不对,拒绝」,余额 1000 到 1000。 同一套转账逻辑,只多了一次比对。 令牌要满足三条:随机、绑会话、短时有效。 一条也不满足的例子很常见——全站一个固定值的「令牌」等于没加,攻击者从自己那份表单里抄一个就行。
三、另外两道:SameSite 与二次验证
SameSite 是 cookie 的属性。
设成 Lax 或 Strict 之后,跨站发起的请求浏览器干脆不带这个 cookie,请求到服务端就是「未登录」,
自然被拒。这一道在浏览器层面,比改代码省事。
本机没有浏览器,这个属性的效果我测不到——它由浏览器执行,Python 的 HTTP 客户端不认识它。
所以这里只讲机制,不给数字,和 11.3.3 讲开放重定向时的处理一样。
二次验证是最后一道:转账、改密码、改绑手机这类操作,要求重新输一次密码或短信码。
就算伪造的请求发出去了,攻击者也过不了这一关。
还有一条常被忽略的:改状态的操作不要用 GET。
GET 只该读数据,这样 <img src> 这条最省事的利用路径就断了。它不完整,但能挡掉一大批。
| 手段 | 挡住什么 | 代价 |
|---|---|---|
| 令牌 | 伪造的跨站请求 | 每个表单带一个隐藏字段 |
| SameSite | 跨站请求带不上 cookie | 从外链跳回来可能掉登录态 |
| 二次验证 | 令牌被绕过也拦得住 | 用户多一步 |
四、动手 Demo:用户什么都没做,钱就出去了
本机起一个银行服务端:/login 发会话,/transfer 改余额。
再模拟一个攻击者的页面——它只做一件事,让浏览器替它提交一个表单。
先跑「没有令牌」的版本,再跑「要求令牌」的版本。
# 攻击者页面里那段东西,等价于这一句
FORM = (u'<form action="/transfer" method="post">'
u'<input name="to" value="attacker">'
u'<input name="amt" value="500"></form>'
u'<script>document.forms[0].submit()</script>')
if need_token:
if q.get(u'token', [u''])[0] != SESS[sid][u'token']:
return 403, u'令牌不对,拒绝'
实跑输出:
--------------------------------------------------------------------
CSRF:用户没做错任何事
--------------------------------------------------------------------
攻击者的页面里放一个自动提交的表单,指向银行的 /transfer。
用户只要打开过银行页面(cookie 还在有效期内),一访问那个恶意页面,
浏览器就会自动带上 cookie 把表单发出去——**用户什么都没点**。
【没有令牌】状态 200,已转 500 给 attacker,余额 500
余额变化 1000 → 500
【要求令牌】状态 403,令牌不对,拒绝
余额变化 1000 → 1000
第一段余额直接少了 500——服务端完全按规矩办事:请求合法、cookie 有效、
方法正确。它唯一没检查的是「这个请求是不是用户自己想发的」。
加令牌就是在问这一句:令牌藏在页面里,第三方站点读不到,所以它伪造不出一个带正确令牌的请求。
还有两件配套的事:一是 `SameSite` 属性,让浏览器在跨站请求时干脆不带 cookie;二是给「转账」这类操作加二次验证。`SameSite` 的效果得靠浏览器实现,本机没有浏览器,我只说机制,不给数字。
第一段:状态 200,正文「已转 500 给 attacker,余额 500」,余额那一行从 1000 掉到 500。
攻击页面没读银行的数据、没碰用户的 cookie,它只是让浏览器发了一个请求。
第二段:403,余额 1000 到 1000,一分没动。
转账逻辑、cookie 校验、参数过滤三样都没改,只多了一次令牌比对。
两个数放在一起才是重点:漏洞不在「代码写得糙」,而在「少问了一句」。
这段代码每一行都符合规范,它只是没问「这请求是不是你发的」。
表里 SameSite 那一条我没实测——它是浏览器执行的行为,本机没有浏览器。
能确定的是:请求到了服务端还是那个请求,变化的是浏览器带不带 cookie。
30 秒实验
- 翻一段你写过的表单代码,看有没有隐藏的令牌字段。
- 想一想:为什么「改状态用 GET」会让伪造请求更容易得手?
- 打开开发者工具,看你常用网站的登录 cookie,
SameSite那一栏是什么值。
常见误区
- 「上了 HTTPS 就防住了」——HTTPS 只管路上不被偷看,请求是浏览器自己发的。
- 「校验 Referer 就够了」——有些环境根本不发 Referer,站点也能主动去掉它。
- 「只有 POST 接口才需要防」——GET 接口只要能改状态,一样能被
<img>打。
自测三问(点开对答案)
1. 这类攻击为什么不需要知道会话 ID? —— 浏览器会自动带上目标站的 cookie,攻击者只要让浏览器发这个请求。 2. 令牌为什么挡得住? —— 令牌写在页面里,第三方站点受同源策略限制读不到,猜不出来。 3. 为什么改状态的操作不该用 GET? —— 一个 `会话这一类讲完了。剩下的四种更杂:猜密码、盖页面、拖服务、留后门。
- ☐ (本节完,下一节 11.5.1 密码破解)