← 返回蜂巢洞察

从零开始学习CSRF:浏览器的工作原理、相关攻击方式以及Spring Security中的实现方法【完整手册】

如果你曾经构建过Web应用程序或配置过Spring Security,那么你几乎肯定遇到过跨站请求伪造攻击(CSRF)。 OAuth 2.0的工作原理:后端开发人员的实用指南 》中,我简要介绍过那个神秘的 state 参数,并指出它的核心作用就是保护授权流程免受CSRF攻击的侵害。 当时,我们只是把CSRF视为一个需要了解的基本概念而已。而今天,我们将深入探讨这一主题。 也许你在使用Spring Boot构建REST API时,会遇到每次发送 POST 请求时都会出现的HTTP 403 Forbidden错误,于是你通过在Security Filter Chain中添加 .csrf(csrf

如果你曾经构建过Web应用程序或配置过Spring Security,那么你几乎肯定遇到过跨站请求伪造攻击(CSRF)。

OAuth 2.0的工作原理:后端开发人员的实用指南》中,我简要介绍过那个神秘的state参数,并指出它的核心作用就是保护授权流程免受CSRF攻击的侵害。

当时,我们只是把CSRF视为一个需要了解的基本概念而已。而今天,我们将深入探讨这一主题。

也许你在使用Spring Boot构建REST API时,会遇到每次发送POST请求时都会出现的HTTP 403 Forbidden错误,于是你通过在Security Filter Chain中添加.csrf(csrf -> csrf.disable())代码来“解决”这个问题。

大多数教程都将CSRF视为一个需要配置的选项,或者是一个框架级别的设置。它们会直接跳到代码示例部分:

// 大多数教程在第一行就会这样写:
http.csrf(Customizer.withDefaults());

从框架配置入手来讲解Web安全机制,其实掩盖了Web安全背后的真正原理。Spring Security并不是凭空创造这些安全规则的,它是根据Web浏览器、HTTP协议以及Cookie的工作原理来制定相应策略的。

在这本手册中,我们将采取自下而上、基于基本原理的方式来讲解Web安全相关内容。在彻底了解浏览器、HTTP头部信息、会话管理机制以及跨站请求伪造攻击的运作原理之前,我们不会讨论Spring Security的相关内容。

读完这本指南后,你将会明白:

  • 为什么浏览器会在发出的请求中自动附加认证信息。

  • 为什么这种自动行为会带来严重的安全漏洞。

  • 为什么攻击者根本不需要窃取或读取你的Cookie就能利用CSRF攻击机制。

  • 为什么同源策略(SOP)和CORS并不能有效防止CSRF攻击。

  • 现代的安全防护措施,比如CSRF令牌和SameSite Cookie,其内部工作原理是什么。

  • Spring Security是如何在内部实现这些安全机制的,以及如何有效地配置它们。

让我们先抛开框架的束缚,来看看Web究竟是如何工作的。

目录

CSRF出现之前的问题

要理解安全机制,我们首先必须了解“状态”这一概念。

超文本传输协议(HTTP)本质上是无状态的。这意味着,如果Alice在上午10点向travelbuddy.com发送了一个HTTP请求,然后在上午10点01分再次发送另一个HTTP请求,服务器会将这两个请求视为完全独立、毫无关联的事件来处理。

序列图显示:Alice的浏览器首先向TravelBuddy服务器发送了一个成功的GET请求,1分钟后又发送了第二个GET请求,但这次收到了401未经授权的错误响应。

如果没有机制来在多次请求之间保持对用户身份的识别,Alice就必须在每一次HTTP请求中都输入自己的用户名和密码。这对用户体验和系统性能来说都是极其不利的。

在会话机制成为标准之前,开发者们尝试通过查询参数或在每次点击时添加基本认证头部来传递用户凭证。这样一来,这些凭证就会被记录在服务器日志、浏览器历史记录以及共享的URL中,从而导致安全风险。

会话和Cookie是如何解决这个问题的?

为了解决这个问题,网页工程师引入了服务器端会话HTTP Cookie这一概念。

当Alice通过POST请求将用户名和密码发送到https://travelbuddy.com/login进行登录时,服务器会验证她的凭证。服务器不会在下一个页面要求她再次登录,而是会在内存中(或数据库/Redis缓存中)创建一个会话,并为这个会话分配一个唯一且不可预测的标识符:即会话ID

随后,服务器会使用一个特殊的HTTP响应头部Set-Cookie将这个会话ID发送回Alice的浏览器。

HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: JSESSIONID=abc123xyz789; Path=/; Secure; HttpOnly

当Alice的浏览器收到这个响应时,它会读取Set-Cookie头部信息,并提取出JSESSIONID=abc123xyz789,将其存储在浏览器的内部存储空间中,也就是Cookie缓存区里。

现在,Alice就已经“登录”成功了。服务器通过这个会话记录来识别她的身份,而浏览器则保存着访问该会话所需的密钥——即JSESSIONID

为什么浏览器会自动发送Cookie

现在我们来到了网页技术早期做出的一个关键设计决策。

一旦浏览器为travelbuddy.com这个域名在Cookie缓存区中存储了JSESSIONID=abc123xyz789,那么在后续的请求中,这个Cookie又是如何被发送回服务器的呢?

开发者是否必须编写自定义的JavaScript代码来添加这些cookie?不需要。

浏览器被专门设计用来自动处理cookie的管理工作。

每当爱丽丝的浏览器准备向https://travelbuddy.com发送HTTP请求时(无论是因为爱丽丝点击了某个链接、提交了HTML表单,还是JavaScript触发了fetch()方法),浏览器都会按照以下步骤进行操作:

  1. 检查目标URL:浏览器会查看目标地址(例如https://travelbuddy.com/api/connections)。

  2. 查找存储的cookie:浏览器会检查其cookie存储区中,那些域名和路径与travelbuddy.com匹配的cookie。

  3. 验证cookie的有效性:浏览器会确认这些cookie是否已经过期,以及诸如Secure这样的安全设置是否得到了遵守。

  4. 将cookie添加到请求头中:如果找到了有效的cookie,浏览器会自动将这些cookie添加到发出的HTTP请求中。

下面是爱丽丝的浏览器发出的请求的实际内容:

POST /api/connections/add HTTP/1.1
Host: travelbuddy.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Accept: text/html,application/xhtml+xml
Cookie: JSESSIONID=abc123xyz789
Content-Type: application/x-www-form-urlencoded

service=SkyScanner

请注意一个关键点:无论是爱丽丝,还是任何自定义的JavaScript代码,都没有显式地添加Cookie: JSESSIONID=abc123xyz789这个cookie。

浏览器在将请求数据发送到网络之前,会自动将其添加到请求头中。对于服务器来说,接收到这个cookie就意味着该请求是来自爱丽丝的已登录会话。

这种自动处理机制非常方便,它使得用户在重新加载页面或点击链接时能够无缝地继续浏览网页。但正如我们接下来将会看到的,这种便利性也会带来安全隐患。

当自动添加cookie变得危险时

那么,自动添加cookie这一行为本身是否构成一种安全漏洞呢?

并不构成。如果爱丽丝只访问travelbuddy.com这个网站,那么这种自动添加cookie的功能是完全正常的。

问题出现在这样一个实际情况中:爱丽丝在同一个浏览器会话中访问了多个网站。

evil.com为例

假设爱丽丝在标签页1中登录到了TravelBuddy网站。她的会话cookie(JSESSIONID=abc123xyz789)被安全地保存在浏览器中,专门用于travelbuddy.com这个网站。

在第二个标签页中,Alice访问了一个与她无关的网站:https://evil.com(也许她是点击了一封钓鱼邮件或论坛帖子中的链接才进入这个网站的)。

evil.com实际上是由攻击者控制的。攻击者知道TravelBuddy有一个用于连接第三方服务的接口,其地址为https://travelbuddy.com/api/connections/add。攻击者想诱使Alice将这个恶意服务连接到她的账户上。

攻击者在evil.com提供的网页中嵌入了以下隐藏的HTML表单:

<!-- 该页面托管在 https://evil.com/win-a-car.html 上 -->
<!DOCTYPE html>
<html>>
<body>
  <h1>您赢了一次免费旅行!请点击下方领取。
  

  <<>script>
    // 页面一加载就自动提交表单
    document.getElementById('maliciousForm').submit();
  
</body>>
</html>>

攻击过程的详细步骤

让我们一步步来看当Alice打开https://evil.com/win-a-car.html时会发生什么:

  1. Alice的浏览器从evil.com获取并解析HTML内容。

  2. 浏览器遇到