创见博客
CSRF 攻击:攻击者如何借你的身份发请求
七崽爱吃小饼干2026/10/11阅读 3专栏 JavaScript

先看一个真实到有点荒诞的场景:

你在银行网站 bank.com 登录成功,然后手贱点开了一个论坛帖,页面里藏着这么一行:

html
<img src="https://bank.com/transfer?to=attacker&money=10000">

你什么都没点,钱转出去了。

整个过程攻击者没有偷到你的密码,也没有读到你的 Cookie。他只是让你的浏览器"顺手"发了一个你自己都不知道的请求。这就是跨站请求伪造(CSRF, Cross-Site Request Forgery)。

这篇文章把 CSRF 讲透:它为什么能成立、能造成什么后果、以及为什么 SameSite 出现后它变得没那么可怕了。

一、一句话讲清 CSRF

CSRF 是攻击者利用浏览器"跨站请求会自动带上目标站 Cookie"这条规则,冒用你已登录的身份,向目标站发一个你不知情的请求。

三个关键词:

  • 跨站(Cross-Site):攻击脚本在攻击者的站点上,不在目标站。
  • 伪造(Forgery):请求看起来是你发的,其实不是你本意。
  • 借身份,不偷身份:攻击者全程拿不到你的 Cookie,只是让浏览器替他带。

理解这一点很关键:CSRF 的目标从来不是"偷数据",而是"借你的手做操作"。

二、成立需要满足哪些条件

不是所有接口都能被 CSRF。必须同时满足:

  1. 你的身份靠 Cookie 自动携带。服务端只看 Cookie 判断你是谁,没有别的验证。
  2. 攻击者知道请求长什么样。URL、方法、参数名都是可猜的(/transfer?to=&amount=)。
  3. 接口会改变状态。转账、改密码、加管理员、发帖这类写操作才有价值。只读接口被伪造也没意义。

再加一个环境条件:你此刻处于登录态,浏览器里存着目标站的有效 Cookie。

只要这几点凑齐,攻击者不需要接触你的账号,只需要你"在登录状态下访问了恶意页面"。

三、两个可复现的例子

3.1 GET 型:一张图片就够

老式的不安全接口常把写操作放到 GET 上:

https://bank.com/transfer?to=attacker&money=10000

攻击者只要在他页面里放一张指向它的图片:

html
<img src="https://bank.com/transfer?to=attacker&money=10000">

图片加载 = 发起请求 = 转账。用户无感,甚至看不到图片(因为返回的不是图片,浏览器只是加载失败,请求照发)。

这也解释了后端的一条铁律:GET 只用来读取,不要用来改变状态。

3.2 POST 型:自动提交表单

现代接口多用 POST,但一样能伪造,因为 HTML 表单天生可以跨站提交:

html
<form action="https://bank.com/transfer" method="POST">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit()</script>

更隐蔽的是,表单还能塞进 0 像素的 iframe 里,页面看起来完全正常。

补充一个常被忽略的点:普通表单只能发 application/x-www-form-urlencoded、multipart/form-data、text/plain 三种类型。想发 application/json 就得用 fetch 并带上自定义头,而这恰好会触发 CORS 预检,被服务端拦下——所以"只接受 JSON 的接口"天然对 CSRF 更安全。但如果后端对 text/plain 也照单全收,这个保护就没了。

四、为什么能成功:Cookie 是"按目标域"携带的

浏览器有一条硬规则:发起请求时,凡是域名匹配目标站的 Cookie,都会被自动带上。

它不看这个请求是用户点的、图片加载的,还是恶意脚本发起的;也不看当前页面在哪个站。只要目标是 bank.com,bank.com 的 Cookie 就会附上。

于是攻击链闭合:

你的浏览器 → 带 bank.com 的 Cookie → 请求发给 bank.com
bank.com   → 看到 Cookie 有效 → 认为是你本人 → 执行操作

为什么"读不到 Cookie"拦不住它

很多人第一反应是:同源策略不是保护我了吗?攻击者根本读不到 bank.com 的 Cookie 啊。

对,读不到——但他也不需要读。浏览器会自动帮他带。同源策略挡住的是"读响应、读数据",挡不住"发请求"。CSRF 从头到尾只需要那个请求被执行,攻击者连响应都不想看。

五、CSRF 能做什么、不能做什么

能做的(只要接口靠 Cookie 鉴权、且是写操作):

  • 转账、下单、充值
  • 修改邮箱 / 密码 / 绑定手机
  • 删帖、发帖、关注、点赞
  • 把你的账号登出(伪造退出请求)
  • 在后台系统里给自己加权限(如果目标站没做防护)

不能做的:

  • 读不到响应内容。跨域限制让攻击者拿不到返回数据,所以他通常不知道操作成没成功,只能"盲打"。
  • 偷不到 Cookie。全程是借用,不是窃取。
  • 拿到你的密码。CSRF 不需要密码。

这也决定了防御思路:既然攻击者依赖"浏览器自动带 Cookie",那就让他带不上,或者让他缺一个拿不到的东西。

六、防御方案

按推荐程度从高到低。

6.1 首选:SameSite Cookie

给 Cookie 加上 SameSite 属性,直接限制它被跨站携带:

Set-Cookie: sessionid=xxx; SameSite=Lax; Secure; HttpOnly

三种取值:

取值行为适用
Strict任何跨站请求都不带最安全,但从外站点链接进来会"掉登录态"
Lax只允许顶级导航 + 安全方法(点链接跳转的 GET)带,POST、iframe、img 一律不带平衡,推荐默认
None不限制,跨站一律带需要第三方嵌入时才用,必须配合 Secure + HTTPS

Lax 是现在的默认值(Chrome 80+ 起,未显式设置 SameSite 的 Cookie 按 Lax 处理),配置成本几乎为零,能挡掉绝大多数 CSRF。注意它仍放行顶级导航的 GET,所以GET 依然不能做写操作。

这是性价比最高的一道防线,新项目应当默认开启。

6.2 标准:CSRF Token

原理是让每个写请求都必须带上一个攻击者拿不到的随机串:

  1. 服务端为当前会话生成一个随机 Token,存在 Session 里。
  2. 前端把它放进表单隐藏域或请求头:
html
<form action="/transfer" method="POST">
  <input type="hidden" name="csrfToken" value="随机串">
  ...
</form>
js
fetch('/transfer', {
  method: 'POST',
  headers: { 'X-CSRF-Token': getCookie('csrfToken') },
  body: JSON.stringify({ to, amount }),
})
  1. 服务端校验请求里的 Token 是否与 Session 中一致,不一致直接拒绝。

攻击者的表单能伪造参数,但跨站页面读不到你站点里的 Token,所以他伪造不出这个字段。这是最经典、最可靠的方案。

它有几个变体:

  • 同步令牌(Synchronizer Token):Token 存服务端 Session,前端从页面拿。
  • 双重提交 Cookie(Double Submit Cookie):服务端种一个随机值到 Cookie(非 HttpOnly),前端读取后放进请求头,服务端比对 Cookie 与请求头是否一致。攻击者能跨站发送 Cookie,但读不到 Cookie 的值,所以无法在请求头里填对。实现简单,但要注意子域 Cookie 注入的风险,生产上常配合签名使用。

6.3 辅助:校验 Origin / Referer

服务端检查请求头里的来源,只放行白名单域名:

js
const origin = req.headers.origin || req.headers.referer
if (!origin || !allowList.some((d) => origin.startsWith(d))) {
  return res.status(403).end()
}

但要搞清它的局限——它不是因为"能被伪造"才不可靠(浏览器自己会设置 Origin/Referer,页面脚本改不了),而是因为它可能根本不存在:

  • Referrer-Policy: no-referrer 会让 Referer 消失。
  • HTTPS 页面请求 HTTP 资源时,Referer 会被浏览器剥掉。
  • 部分场景下 Origin 头也可能缺失。

所以它适合当纵深防御,不能作为唯一手段。

6.4 兜底:二次验证

对被 CSRF 成功代价极高的操作(改密码、转账、删除账号),要求输入密码或短信验证码。攻击者能伪造请求,但伪造不出验证码,攻击链直接断掉。缺点就是体验差,只该用在敏感操作上。

6.5 从根上绕开:别用 Cookie 承载身份

如果接口鉴权用的是放在 Authorization 头里的 Token(比如 JWT),而不是 Cookie,那 CSRF 基本不成立——因为浏览器不会自动附带 Authorization 头,攻击者的跨站请求带不上它。代价是你要自己管理 Token 的存储与刷新,并注意 XSS 风险(那是另一个话题了)。

七、速查表

方案原理优点局限推荐度
SameSite=Lax/Strict限制跨站携带 Cookie配置简单、浏览器原生老浏览器不支持;Lax 仍放行顶级 GET高
CSRF Token请求必须带攻击者拿不到的随机串最可靠、标准化需前后端配合高
双重提交 CookieCookie 值再手动带回请求头比对无需服务端存 Token子域注入风险,需签名加固中
Origin / Referer 校验校验请求来源白名单实现简单头可能缺失,不能单独依赖中
二次验证人工确认敏感操作极安全体验差敏感操作专用
用 Authorization 头鉴权浏览器不自动附带该头从根上免疫需自行管理 Token,防 XSS视架构

小结

  • CSRF 的本质:攻击者借你的浏览器、你的 Cookie,替你发一个你不知情的写请求。他不偷密码,也不读数据,只借身份。
  • 它能成立,全靠浏览器那条规则:按目标域自动携带 Cookie。同源策略挡得住"读",挡不住"发"。
  • 最有效的防御是让跨站请求要么带不上 Cookie(SameSite=Lax/Strict),要么缺一个攻击者拿不到的东西(CSRF Token)。
  • Origin / Referer 校验可以作为补充,但不能单独依赖,因为它可能被策略删掉。
  • 顺手记住两条铁律:GET 不做写操作,写操作必须带 Token。

一句话:CSRF 防的不是"偷",而是"冒充"——让伪造的请求带不上身份,或缺了那把只有真实页面才有的钥匙。

评论
0/100