CSRF 攻击 学习

104 阅读16分钟

陌生链接不要随便点。

黑客如何才能拿到你的cookie?

浏览网页的时候使用的是http协议,而http是无状态的协议。

用户先登录自己的谷歌邮箱,然后 Gmail 服务器返回一些登录状态给 David 的浏览器,这些信息包括了 Cookie、Session 等,这样在 David 的浏览器中,Gmail 邮箱就处于登录状态了。然后打开 Gmail 邮箱中的一份邮件,并点击了该邮件中的一个链接。过了几天,David 就发现他的域名被盗了。后面David 还是要回了他的域名,也弄清楚了他的域名之所以被盗,就是因为无意间点击的那个链接。

image.png

David 的域名被盗流程:

  • 用户访问某个需要登录的网站,并且网站会返回一些登陆状态信息给浏览器存储下来(Cookie、Session)
  • 用户通过在该需要登陆的网站中点击了其他站点的链接并跳转到那个域名下对应的页面中
  • 而黑客提前在这个准备的页面中,伪造一些请求(域名都是用户那个站点的域名)并传一些参数给用户的受信站点去,因为浏览器默认行为是:即使是在别的域名站点(黑客的)下,如果发出针对其他域名的请求,只要当前这个浏览器中已经有了其他域名下的cookie等,那么也会读取其他站点下的这些信息,然后自动带着cookie等信息去请求对应站点,所以相当于间接获取到用户的登录状态信息,伪造用户的请求到用户登录的那个网站中去进行一些操作

跨站请求伪造 (CSRF, Cross-Site Request Forgery) 攻击的核心原理在于:攻击者诱导用户在登录状态下访问恶意站点,在黑客的网站中,利用用户的登录状态发起跨站请求,然后利用用户的身份认证信息(如 Cookies)来对用户已经登录的网站发起恶意请求。

通常当用户打开了黑客的页面后,黑客有三种方式去实施 CSRF 攻击。

例子

这里假设某个正常站点具有转账功能,可以通过 POST 或 Get 来实现转账,转账接口如下所示:

 # 同时支持 POST 和 Get
 # 接口
 https://time.org/sendcoin
 # 参数
 ## 目标用户
 user
 ## 目标金额
 number

黑客需要知道的信息:

  1. 请求需要发送到的目标站点及其接口
  2. 请求的方式(get或者post)
  3. 请求时需要发送的数据

1. 自动发起 Get 请求

 这个页面是黑客的恶意页面,比如是在www.hacker.com/index.html页面
 <!DOCTYPE html>
 <html>
   <body>
     <h1>黑客的站点:CSRF 攻击演示</h1>
     <img src="https://time.geekbang.org/sendcoin?user=hacker&number=100" />
   </body>
 </html>

黑客页面的 HTML 代码,在这段代码中,黑客将转账的请求接口隐藏在 img 标签内, 欺骗浏览器这是一张图片资源。当该页面被加载时,浏览器会自动发起 img 的资源请求(因为用户的已经在这个站点登陆过,所以浏览器的默认行为是会带着这个站点的登录信息,比如cookie发送相应的请求到对应的网站上),如果服务器没有对该请求做判断的话,那么服务器就会认为该请求是一个转账请求,于是用户账户上的 100 极客币就被转移到黑客的账户上去。

或者使用表单元素: image.png

2. 自动发起 POST 请求

有些服务器的接口是使用 POST 方法的,所以黑客还需要在他的站点上伪造 POST 请求,当用户打开黑客的站点时,是自动提交 POST 请求,具体的方式你可以参考下面示例代码:

 <!DOCTYPE html>
 <html>
   <body>
     <h1> 黑客的站点:CSRF 攻击演示 </h1>
     <form id='hacker-form' action="https://time.geekbang.org/sendcoin" method="POST">
       <input type="hidden" name="user" value="hacker" />
       <input type="hidden" name="number" value="100" />
     </form>
     <script> document.getElementById('hacker-form').submit(); </script>
   </body>
 </html>

黑客在他的页面中构建了一个隐藏的表单,该表单的内容就是极客时间的转账接口。当用户打开该站点之后,这个表单会被自动执行提交;当表单被提交之后,服务器就会执行转账操作。因此使用构建自动提交表单这种方式,就可以自动实现跨站点 POST 数据提交。

3. 引诱用户点击链接

还有一种诱惑用户点击黑客站点上的链接,这种方式通常出现在论坛或者恶意邮件上。黑客会采用很多方式去诱惑用户点击链接,示例代码如下所示:

 <div>
   <img width=150 src="http://images.xuejuzi.cn/1612/1_161230185104_1.jpg"> </img>
 <a href="https://time.geekbang.org/sendcoin?user=hacker&number=100" taget="_blank"
    点击下载美女照片
    </a>
 </div>

这段黑客站点代码,页面上放了一张美女图片,下面放了图片下载地址,而这个下载地址实际上是黑客用来转账的接口,一旦用户点击了这个链接,那么他的极客币就被转到黑客账户。

和 XSS 不同的是,CSRF 攻击不需要将恶意代码注入用户的页面,仅仅是利用服务器的漏洞和用户的登录状态来实施攻击。

CSRF 攻击能造成的影响需要看站点对应的存在漏洞的接口有哪些功能,比如:

  1. 修改用户信息
  2. 转账

因为是黑客冒充用户发送的请求,因此是请求伪造(Request Forgery),并且场景设计两个跨域的站点(Cross Site),简称CSRF攻击。

防止 CSRF 攻击

  1. 不要访问莫名的网站
  2. 使用没有使用过的浏览器(因为没有在这个浏览器中有登录记录)
  3. 每次登录后登出网站
  4. 交给开发者来防御

以上四种方法。

CSRF 攻击的一些“特征”,发起 CSRF 攻击的三个必要条件:

  1. 目标站点一定要有 CSRF 漏洞;
  2. 用户要登录过目标站点,并且在浏览器上保持有该站点的登录状态;
  3. 需要用户打开一个第三方站点,可以是黑客的站点,也可以是一些论坛。

黑客是无法通过 CSRF 攻击来获取用户页面数据的;其最关键的一点是要能找到服务器的漏洞,所以说对于 CSRF 攻击主要的防护手段是提升服务器的安全性。

防止 CSRF 攻击的手段:

利用好 Cookie 的 SameSite 属性

黑客会利用用户的登录状态来发起 CSRF 攻击,而 Cookie 正是浏览器和服务器之间维护登录状态的一个关键数据,因此要阻止 CSRF 攻击, 我们首先就要考虑在 Cookie 上来做文章。

通常 CSRF 攻击都是从第三方站点发起的,要防止 CSRF 攻击,我们最好能实现从第三方站点发送请求时禁止 Cookie 的发送,因此在浏览器通过不同来源发送 HTTP 请求时,有如下区别:

  • 如果是从第三方站点发起的请求,那么需要浏览器禁止发送某些关键 Cookie 数据到服 务器;
  • 如果是同一个站点发起的请求,那么就需要保证 Cookie 数据正常发送。

Cookie 中的 SameSite 属性正是为了解决这个问题的,通过使用 SameSite 可以有效地降低 CSRF 攻击的风险。

SameSite 是怎么防止 CSRF 攻击的呢?

HTTP是没有状态的,但为了保存状态,网景公司发明了cookie用来记录用户的状态信息。但是存在一个弊端,就是我们网站 A 的cookie可以作为第三方网站的cookie去使用。这样就造成了CSRF的漏洞SameSite就可以限制第三方cookie的使用。

在 HTTP 响应头中,通过 set-cookie 字段设置 Cookie 时,可以带上 SameSite 选项,如下:

 Set-Cookie: CookieName=CookieValue; SameSite=strict;

SameSite 选项通常有 Strict、Lax 和 None 三个值。

  • Strict:完全禁止第三方 Cookie,也就是在跨站时,均不会携带cookie,只有当前站点的url和访问的站点的url一致时,才能携带cookie。简言之,如果你从极客时间的页面中访问 InfoQ 的资源,而 InfoQ 的某些 Cookie 设置了 SameSite = Strict 的话,那么这些 Cookie 是不会被发送到 InfoQ 的服务器上的。只有你从 InfoQ 的站点去请求 InfoQ 的资源时,才会带上这些 Cookie。

    作者回复: 我把整个流程写一遍:

    首先假设你发出登录 InfoQ 的站点请求,然后在 InfoQ 返回 HTTP 响应头给浏览器,InfoQ 响应头中的某些 set-cookie 字段如下所示:

    set-cookie: a_value=avalue_xxx; expires=Thu, 21-Nov-2019 03:53:16 GMT; path=/; domai n=.infoq.com; SameSite=strict

    set-cookie: b_value=bvalue_xxx; expires=Thu, 21-Nov-2019 03:53:16 GMT; path=/; domai n=.infoq.com; SameSite=lax

    set-cookie: c_value=cvaule_xxx; expires=Thu, 21-Nov-2019 03:53:16 GMT; path=/; domai n=.infoq.com; SameSite=none

    set-cookie: d_value=dvaule_xxxx; expires=Thu, 21-Nov-2019 03:53:16 GMT; path=/; dom ain=.infoq.com; 我们可以看出,

    a_value 的 SameSite 属性设置成了 strict,

    b_value 的 SameSite 属性设置成了 lax

    c_value 的 SameSite 属性值设置成了 none

    d_value 没有设置 SameSite 属性值

    好,这些 Cookie 设置好之后,当你再次在 InfoQ 的页面内部请求 InfoQ 的资源时,这些 Cookie 信 息都会被附加到 HTTP 的请求头中,如下所示: cookie: a_value=avalue_xxx;b_value=bvalue_xxx;c_value=cvaule_xxx;d_value=dvaule_xxxx;

    但是,假如你从 time.geekbang.org 的页面中,通过 a 标签打开页面,如下所示:<a href="https://www.infoq.cn/sendcoin?user=hacker&number=100">点我下载</a>(https://www.infoq.cn/sendcoin?user=hacker&number=100)当用户点击整个链接的时候,因为 InfoQ 中 a_vaule 的 SameSite 的值设置成了 strict,那么 a_vaule 的值将不会被携带到这个请求的 HTTP 头中。

    如果 time.geekbang.org 的页面中,有通过 img 来加载的 infoq 的资源代码,如下所示:

    <img src="https://www.infoq.cn/sendcoin?user=hacker&number=100" >

    那么在加载 infoQ 资源的时候,只会携带 c_value,和 d_value 的值。

  • Lax:在跨站点的情况下,从第三方站点的链接打开和从第三方站点提交 Get 方式的表单这两种方式都会携带 Cookie。但如果在第三方站点中使用 Post 方法, 或者通过 img、iframe 等标签加载的 URL,这些场景都不会携带 Cookie。

导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。

请求类型示例正常情况Lax
链接<a href="..."></a>发送 Cookie发送 Cookie
预加载<link rel="prerender" href="..."/>发送 Cookie发送 Cookie
GET 表单<form method="GET" action="...">发送 Cookie发送 Cookie
POST 表单<form method="POST" action="...">发送 Cookie不发送
iframe<iframe src="..."></iframe>发送 Cookie不发送
AJAX$.get("...")发送 Cookie不发送
Image<img src="...">发送 Cookie不发送

设置了StrictLax以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。

  • None,在任何情况下都会发送 Cookie 数据

对于防范 CSRF 攻击,可以针对实际情况将一些关键的 Cookie 设置为 Strict 或者 Lax 模式,这样在跨站点请求时,这些关键的 Cookie 就不会被发送到服务器,从而使得黑客的 CSRF 攻击失效。

验证请求的来源站点

在服务器端验证请求来源的站点。由于 CSRF 攻击大多来自于第三方站点,因此服务器可以禁止来自第三方站点的请求。那么该怎么判断请求是否来自第三方站点呢?

通过 HTTP 请求头中的 Referer 和 Origin 属性。

Referer 是 HTTP 请求头中的一个字段,记录了该 HTTP 请求的来源地址。比如我从极客 时间的官网打开了 InfoQ 的站点,那么请求头中的 Referer 值是极客时间的 URL,如下 图:

image-20220709184203088转存失败,建议直接上传图片文件

虽然可以通过 Referer 告诉服务器 HTTP 请求的来源,但是有一些场景是不适合将来源 URL 暴露给服务器的,因此浏览器提供给开发者一个选项,可以不用上传 Referer 值,具 体可参考 Referrer Policy。

但在服务器端验证请求头中的 Referer 并不是太可靠,因此标准委员会又制定了 Origin 属 性,在一些重要的场合,比如通过 XMLHttpRequest、Fecth 发起跨站请求或者通过 Post 方法发送请求时,都会带上 Origin 属性,如下图:

image-20220709184310600转存失败,建议直接上传图片文件

Origin 属性只包含了域名信息,并没有包含具体的 URL 路径,这是 Origin 和 Referer 的一个主要区别。在这里需要补充一点,Origin 的值之所以不包含详细 路径信息,是有些站点因为安全考虑,不想把源站点的详细路径暴露给服务器。

因此,服务器的策略是优先判断 Origin,如果请求头中没有包含 Origin 属性,再根据实际 情况判断是否使用 Referer 值。

CSRF Token

核心原理是通过验证请求是否携带合法且唯一的 Token,确保请求是由用户主动且明确发起的,而非被恶意网站伪造的。

CSRF Token 的核心思想

  • 唯一性:为每个用户会话或请求生成一个随机且不可预测的 Token
  • 关联性:Token 与用户当前会话(Session)绑定。
  • 验证性:服务器要求客户端在发起敏感请求时必须提交此 Token,否则拒绝请求。

CSRF Token 的生成和绑定方式可以根据具体实现有所不同,但核心思想是 确保每个敏感请求(或会话)携带一个唯一且不可预测的 Token

1. CSRF Token 的生成时机

CSRF Token 的生成和绑定通常有两种模式,具体取决于安全需求:

模式 A:会话级 Token(Session-based Token)

  • 生成时机:在用户 登录时首次访问敏感页面时 生成。
  • 唯一性:同一个用户会话(Session)中,Token 保持不变,直到会话过期或重新生成。
  • 验证逻辑:服务器将 Token 存储在 Session 中,后续所有请求只需验证 Token 是否与 Session 中的一致。
  • 适用场景:适用于一般安全需求的系统,实现简单。

模式 B:请求级 Token(Per-Request Token)

  • 生成时机每次页面加载(或每次请求) 时生成新 Token。
  • 唯一性:每个页面或请求的 Token 完全不同,且通常一次性有效(使用后立即失效)。
  • 验证逻辑:服务器需要记录最近生成的有效 Token(如存储在缓存中),验证后立即标记为失效。
  • 适用场景:对安全性要求极高的系统(如金融操作),但实现复杂,可能影响用户体验(如浏览器后退按钮失效)。

每次访问页面生成 Token

如果系统设计为 请求级 Token(模式 B),那么:

  1. 用户每次访问页面时,服务器生成一个新的唯一 Token。
  2. 该 Token 仅对本次页面访问后的首次提交有效(例如提交表单时)。
  3. 如果用户刷新页面,会生成新的 Token,旧 Token 失效。

这种设计下,Token 的生命周期极短,安全性更高,但需要解决以下问题:

  • 服务器存储压力:需记录大量短期有效的 Token(可通过加密签名解决,如 JWT)。
  • 用户并行操作:例如打开多个标签页提交表单,需确保每个标签页的 Token 独立有效。

实际常见实现

大多数系统采用 会话级 Token(模式 A)的折中方案:

  • 用户登录时生成一个 Token,存入 Session。
  • 该 Token 在整个会话期间有效,直到用户注销或 Session 过期。
  • 所有敏感请求(如 POST 请求)均验证此 Token。

这种模式平衡了安全性和实现复杂度,但需注意:

  • 如果 Token 泄露(如被 XSS 攻击窃取),攻击者仍可伪造请求。
  • 因此,需结合其他安全措施(如设置 Cookie 的 SameSite 属性、输入验证等)。

示例对比

场景 1:会话级 Token

  • 用户登录 → 生成 Token ABC,存入 Session。
  • 用户访问表单页 → 页面中嵌入 Token ABC
  • 用户提交表单 → 携带 Token ABC,服务器验证通过。
  • 用户刷新表单页 → Token 仍为 ABC(未重新生成)。

场景 2:请求级 Token

  • 用户访问表单页 → 生成 Token XYZ,存入缓存。
  • 用户提交表单 → 携带 Token XYZ,服务器验证后标记为失效。
  • 用户刷新表单页 → 生成新 Token 123,旧 Token XYZ 失效。
工作流程
  1. 生成 Token

    • 用户登录时,服务器生成一个随机 Token(如 a1b2c3d4e5),存储在服务器端的(如 Session)或加密的 Cookie 中。
    • 服务器将 Token 嵌入到返回给用户的页面中(如表单的隐藏字段、HTTP 响应头等)。
  2. 客户端携带 Token

    • 用户在提交表单或发起敏感请求(如 POST 请求)时,必须携带此 Token。例如:

       <form action="/transfer" method="POST">
         <input type="hidden" name="csrf_token" value="a1b2c3d4e5">
         <!-- 其他表单字段 -->
       </form>
      

      运行 HTML

    • 或在 AJAX 请求的 HTTP 头中添加 Token:

       headers: {
         'X-CSRF-Token': 'a1b2c3d4e5'
       }
      
  3. 服务器验证 Token

    • 服务器收到请求后,从请求参数或头中提取 Token,并与服务器存储的 Token 比对。
    • 如果 Token 匹配且未过期,请求被视为合法;否则拒绝执行操作。

关键设计要点
  • 不可预测性:Token 必须使用安全的随机数生成(如加密算法),防止攻击者猜测。
  • 会话绑定:Token 通常与用户 Session 关联,每个会话或请求独立生成。
  • 安全传输:服务端的 CSRF Token 不能通过 Cookie 传输(避免与 CSRF 攻击的 Cookie 自动携带冲突),应通过表单字段、HTTP 头等方式传递。
  • 短期有效:Token 可设置为一次性使用或短期有效,增强安全性。

第一步,在浏览器向服务器发起请求时,服务器生成一个 CSRF Token。CSRF Token 其实 就是服务器生成的字符串,然后将该字符串植入到返回的页面中。你可以参考下面示例代 码:

 !DOCTYPE html>
 <html>
   <body>
     <form action="https://time.geekbang.org/sendcoin" method="POST">
       <input type="hidden" name="csrf-token" value="nc98P987bcpncYhoadjoiydc9aj">
       <input type="text" name="user" />
       <input type="text" name="number" />
       <input type="submit" />
     </form>
   </body>
 </html>

第二步,在浏览器端如果要发起转账的请求,那么需要带上页面中的 CSRF Token,然后服 务器会验证该 Token 是否合法。如果是从第三方站点发出的请求,那么将无法实时获取到新的 CSRF Token 的值,所以即使发出了请求,服务器也会因为 CSRF Token 不正确而拒绝请求。

如果是 CSRF 攻击,那么黑客是拿不到受害者站点数据的。

但是黑客会在他的 A 站点中调用受害者 B 站点的 http 接口,这些接口可以是转账,删帖或者设置等。

这个过程中你需要注意一点,在黑客 A 站点中调用受害者 B 站点的 http 接口时,默认情况下,浏览器依然会把受害者的 Cookie 等信息数据发送到受害者的 B 站点,【注意这里并不是黑客的 A 站点】。

如果 B 站点存在漏洞的话,那么黑客就会攻击成功,比如将受害者的金币转出去!