# 跨站点请求伪造

Spring 为防范[跨站点请求伪造](https://en.wikipedia.org/wiki/Cross-site_request_forgery)攻击提供了全面的支持。在以下几节中，我们将探讨：

* [什么是 CSRF 攻击？](#csrf-explained)

* [防范 CSRF 攻击](#csrf-protection)

* [CSRF 考虑因素](#csrf-considerations)

|   |这部分的文档讨论了 CSRF 保护的一般主题。<br/>关于基于[servlet](../../servlet/exploits/csrf.html#servlet-csrf)和[WebFlux](../../reactive/exploits/csrf.html#webflux-csrf)的应用程序的 CSRF 保护的具体信息，请参阅相关章节。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## 什么是 CSRF 攻击？

理解 CSRF 攻击的最好方法是看一个具体的例子。

假设你的银行的网站提供了一种表单，允许将当前登录的用户的资金转移到另一个银行帐户。例如，传输表单可能看起来像：

例 1。转移形式

```
<form method="post"
	action="/transfer">
<input type="text"
	name="amount"/>
<input type="text"
	name="routingNumber"/>
<input type="text"
	name="account"/>
<input type="submit"
	value="Transfer"/>
</form>
```

相应的 HTTP 请求可能看起来像：

例 2。传输 HTTP 请求

```
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876
```

现在，假装你对银行的网站进行了认证，然后在不登出的情况下，访问一个邪恶的网站。邪恶网站包含一个 HTML 页面，其形式如下：

例 3。邪恶转移形式

```
<form method="post"
	action="https://bank.example.com/transfer">
<input type="hidden"
	name="amount"
	value="100.00"/>
<input type="hidden"
	name="routingNumber"
	value="evilsRoutingNumber"/>
<input type="hidden"
	name="account"
	value="evilsAccountNumber"/>
<input type="submit"
	value="Win Money!"/>
</form>
```

你想赢钱，所以你点击提交按钮。在此过程中，你无意中向恶意用户转移了 100 美元。发生这种情况的原因是，虽然邪恶的网站无法看到你的 cookie，但与你的银行相关的 cookie 仍会随请求一起发送。

最糟糕的是，整个过程都可以使用 JavaScript 实现自动化。这意味着你甚至不需要点击这个按钮。此外，当访问一个受[XSS attack](https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)影响的诚实网站时，也很容易发生这种情况。那么，我们如何保护我们的用户免受此类攻击呢？

## 防范 CSRF 攻击

可能发生 CSRF 攻击的原因是，来自受害者网站的 HTTP 请求和来自攻击者网站的请求完全相同。这意味着，无法拒绝来自邪恶网站的请求，也无法允许来自该行网站的请求。为了防止 CSRF 攻击，我们需要确保请求中有邪恶站点无法提供的内容，因此我们可以区分这两个请求。

Spring 提供了两种机制来防止 CSRF 攻击：

* the[同步器令牌模式](#csrf-protection-stp)

* 在会话cookie 上指定[Samesite 属性](#csrf-protection-ssa)

|   |这两种保护都要求[安全的方法必须是幂等的。](#csrf-protection-idempotent)|
|---|--------------------------------------------------------------------------------------------|

### 安全的方法必须是幂等的。

为了使针对 CSRF 的[要么保护](#csrf-protection)工作，应用程序必须确保[“安全的”HTTP 方法是幂等的](https://tools.ietf.org/html/rfc7231#section-4.2.1)。这意味着使用 http 方法`GET`、`HEAD`、`OPTIONS`和`TRACE`的请求不应改变应用程序的状态。

### 同步器令牌模式

防止 CSRF 攻击的主要和最全面的方法是使用[同步器令牌模式](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern)。这种解决方案是为了确保每个 HTTP 请求，除了我们的会话cookie 之外，还必须在 HTTP 请求中存在一个安全的随机生成值，称为 CSRF 令牌。

当提交 HTTP 请求时，服务器必须查找预期的 CSRF 令牌，并将其与 HTTP 请求中的实际 CSRF 令牌进行比较。如果值不匹配，则应拒绝 HTTP 请求。

这一工作的关键是，实际的 CSRF 令牌应该位于 HTTP 请求的一部分，该部分不会被浏览器自动包含。例如，在 HTTP 参数或 HTTP 报头中要求实际的 CSRF 令牌，可以防止 CSRF 攻击。在 Cookie 中要求实际的 CSRF 令牌是不起作用的，因为浏览器会自动将 Cookie 包含在 HTTP 请求中。

我们可以放松预期，只需要每个更新应用程序状态的 HTTP 请求的实际 CSRF 令牌。要使其工作，我们的应用程序必须确保[安全的 HTTP 方法是幂等的](#csrf-protection-idempotent)。这提高了可用性，因为我们希望允许使用外部站点的链接来链接到我们的网站。此外，我们不想在 HTTP GET 中包含随机令牌，因为这可能导致令牌泄漏。

让我们来看看使用 Synchronizer 令牌模式时[我们的例子](#csrf-explained)将如何更改。假设实际的 CSRF 令牌需要位于一个名为`_csrf`的 HTTP 参数中。我们的申请的转移表看起来是这样的：

例 4。同步器令牌形式

```
<form method="post"
	action="/transfer">
<input type="hidden"
	name="_csrf"
	value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<input type="text"
	name="amount"/>
<input type="text"
	name="routingNumber"/>
<input type="hidden"
	name="account"/>
<input type="submit"
	value="Transfer"/>
</form>
```

表单现在包含一个隐藏的输入，其值为 CSRF 令牌。外部站点无法读取 CSRF 令牌，因为相同的源策略确保邪恶站点无法读取响应。

相应的转移资金的 HTTP 请求将如下所示：

例 5。同步器令牌请求

```
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
```

你将注意到，HTTP 请求现在包含带有安全随机值的`_csrf`参数。当服务器将实际的 CSRF 令牌与预期的 CSRF 令牌进行比较时，Evil 网站将无法为`_csrf`参数（必须在 Evil 网站上显式提供）提供正确的值，并且传输将失败。

### Samesite 属性

防止[CSRF 攻击](#csrf)的一种新兴方法是在 cookie 上指定[Samesite 属性](https://tools.ietf.org/html/draft-west-first-party-cookies)。服务器在设置 cookie 时可以指定`SameSite`属性，以指示当来自外部站点时不应发送 cookie。

|   |Spring 安全性并不直接控制会话cookie 的创建，因此它不提供对 Samesite 属性的支持。[Spring Session](https://spring.io/projects/spring-session)在基于 Servlet 的应用程序中提供了对`SameSite`属性的支持。<br/> Spring 框架的[CookiewebessionDresolver](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/server/session/CookieWebSessionIdResolver.html)在基于 WebFlux 的应用程序中提供了对`SameSite`属性的开箱即用支持。|
|---| |

例如，具有`SameSite`属性的 HTTP 响应头可能看起来如下所示：

例 6。Samesite HTTP 响应

```
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
```

`SameSite`属性的有效值是：

* `Strict`-当指定时，来自[same-site](https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-2.1)的任何请求都将包括 cookie。否则，Cookie 将不会包含在 HTTP 请求中。

* `Lax`-当指定的 cookie 来自[same-site](https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-2.1)或当请求来自顶级导航和[方法是幂等的](#csrf-protection-idempotent)时将被发送。否则，Cookie 将不会包含在 HTTP 请求中。

让我们来看看如何使用[我们的例子](#csrf-explained)属性来保护`SameSite`。银行应用程序可以通过在会话cookie 上指定`SameSite`属性来防止 CSRF。

通过在我们的会话cookie 上设置`SameSite`属性，浏览器将继续发送来自银行网站的请求的`JSESSIONID`cookie。但是，浏览器将不再发送带有来自 Evil 网站的传输请求的`JSESSIONID`cookie。由于会话在来自邪恶网站的传输请求中不再存在，因此应用程序受到 CSRF 攻击的保护。

在使用`SameSite`属性来防止 CSRF 攻击时，应该注意一些重要的[注意事项](https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-5)属性。

将`SameSite`属性设置为`Strict`提供了更强的防御能力，但可能会使用户感到困惑。考虑一个用户保持登录到托管在[https://social.example.com](https://social.example.com)的社交媒体网站。用户在[https://email.example.org](https://email.example.org)处收到一封电子邮件，其中包括一个到该社交媒体网站的链接。如果用户点击了该链接，他们理所当然地希望得到该社交媒体网站的认证。但是，如果`SameSite`属性是`Strict`，则不会发送 cookie，因此不会对用户进行身份验证。

|   |通过实现[gh-7537](https://github.com/spring-projects/spring-security/issues/7537)，可以提高`SameSite`对 CSRF 攻击的防护能力和可用性。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

另一个明显的考虑因素是，为了让`SameSite`属性保护用户，浏览器必须支持`SameSite`属性。大多数现代浏览器都[支持 Samesite 属性](https://developer.mozilla.org/en-US/docs/Web/HTTP/headers/Set-Cookie#Browser_compatibility)。然而，仍在使用的旧浏览器可能不会。

出于这个原因，通常建议使用`SameSite`属性作为深度防御，而不是针对 CSRF 攻击的唯一保护。

## 何时使用 CSRF 保护

你什么时候应该使用 CSRF 保护？我们的建议是，对于普通用户可能通过浏览器处理的任何请求，使用 CSRF 保护。如果你只是创建一个由非浏览器客户端使用的服务，那么你可能希望禁用 CSRF 保护。

### CSRF 保护和 JSON

一个常见的问题是“我需要保护 JavaScript 发出的 JSON 请求吗？”简短的回答是，这要看情况而定。但是，你必须非常小心，因为有 CSRF 漏洞可能会影响 JSON 请求。例如，恶意用户可以创建[使用以下表单使用 JSON 的 CSRF](http://blog.opensecurityresearch.com/2012/02/json-csrf-with-parameter-padding.html):

例 7。使用 JSON 表单的 CSRF

```
<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
	<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
	<input type="submit"
		value="Win Money!"/>
</form>
```

这将产生以下 JSON 结构

例 8。CSRF 与 JSON 请求

```
{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}
```

如果应用程序没有验证 Content-type，那么它就会受到此攻击。根据设置，验证 Content-type 的 Spring MVC 应用程序仍然可以通过将 URL 后缀更新为`.json`来利用该漏洞，如下所示：

例 9。使用 JSON Spring MVC 表单的 CSRF

```
<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">
	<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
	<input type="submit"
		value="Win Money!"/>
</form>
```

### CSRF 和无状态浏览器应用程序

如果我的应用程序是无状态的，该怎么办？这并不一定意味着你受到了保护。实际上，如果用户不需要在 Web 浏览器中为给定的请求执行任何操作，那么他们很可能仍然容易受到 CSRF 攻击。

例如，考虑一个应用程序，它使用一个自定义 Cookie 来进行身份验证，而不是使用 JSessionID。当发生 CSRF 攻击时，将以与我们上一个示例中发送 JSessionID Cookie 相同的方式发送自定义 Cookie 和请求。此应用程序易受 CSRF 攻击。

使用基本身份验证的应用程序也容易受到 CSRF 攻击。该应用程序很容易受到攻击，因为浏览器将在任何请求中自动包括用户名和密码，其方式与我们上一个示例中发送的 JSessionID cookie 的方式相同。

## CSRF 考虑因素

在实施针对 CSRF 攻击的保护时，有几个特殊的考虑因素需要考虑。

### 登录

为了防止[请求中的伪造日志](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Forging_login_requests)，应该保护 HTTP 请求中的日志不受 CSRF 攻击。防止在请求中伪造日志是必要的，这样恶意用户就无法读取受害者的敏感信息。攻击的执行方式如下：

* 恶意用户使用恶意用户的凭据执行 CSRF 登录。受害者现在被认证为恶意用户。

* 然后，恶意用户诱使受害者访问受损网站并输入敏感信息。

* 该信息与恶意用户的帐户相关联，因此恶意用户可以使用自己的凭据登录并查看 Vicitim 的敏感信息。

确保登录 HTTP 请求不受 CSRF 攻击的一个可能的复杂情况是，用户可能会经历会话超时，这会导致请求被拒绝。会话超时对于那些预计不需要会话才能登录的用户来说是令人惊讶的。有关更多信息，请参见[CSRF 和会话暂停](#csrf-considerations-timeouts)。

### 注销

为了防止伪造注销请求，应该保护注销 HTTP 请求不受 CSRF 攻击。防止伪造注销请求是必要的，这样恶意用户就无法读取受害者的敏感信息。有关攻击的详细信息，请参阅[这篇博文](https://labs.detectify.com/2017/03/15/loginlogout-csrf-time-to-reconsider/)。

确保退出 HTTP 请求不受 CSRF 攻击的一个可能的复杂情况是，用户可能会经历会话超时，这会导致请求被拒绝。会话超时对于那些不期望为了注销而需要会话超时的用户来说是令人惊讶的。有关更多信息，请参见[CSRF and Session Timeouts](#csrf-considerations-timeouts)。

### CSRF and Session Timeouts

通常，期望的 CSRF 令牌存储在会话中。这意味着，一旦会话过期，服务器将找不到预期的 CSRF 令牌并拒绝 HTTP 请求。解决超时问题有多种选择，每种选择都需要权衡利弊。

* 减少超时的最佳方法是使用 JavaScript 在表单提交时请求 CSRF 令牌。然后用 CSRF 令牌更新表单并提交表单。

* 另一种选择是使用一些 JavaScript，让用户知道他们的会话即将到期。用户可以单击按钮继续并刷新会话。

* 最后，预期的 CSRF 令牌可以存储在 Cookie 中。这允许预期的 CSRF 令牌比会话有效。

  有人可能会问，为什么预期的 CSRF 令牌在默认情况下没有存储在 Cookie 中。这是因为存在已知的利用漏洞攻击，其中的头（例如，指定 cookie）可以由另一个域设置。这也是 Ruby on Rails[当标题 x-request-with 出现时，不再跳过 CSRF 检查](https://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails/)的原因。有关如何执行此漏洞利用的详细信息，请参见[这个 webappsec.org 线程](https://web.archive.org/web/20210221120355/https://lists.webappsec.org/pipermail/websecurity_lists.webappsec.org/2011-February/007533.html)。另一个缺点是，通过删除状态（即超时），如果令牌受到损害，你将失去强制使其无效的能力。

### 

保护多部分请求（文件上传）不受 CSRF 攻击会导致[鸡和蛋](https://en.wikipedia.org/wiki/Chicken_or_the_egg)问题。为了防止 CSRF 攻击的发生，必须读取 HTTP 请求的主体以获得实际的 CSRF 令牌。然而，读取主体意味着文件将被上传，这意味着外部站点可以上传文件。

有两个选择使用 CSRF 保护与多部分/形式数据。每一种选择都有其利弊得失。

* [将 CSRF 标记放入体内](#csrf-considerations-multipart-body)

* [在 URL 中放置 CSRF 令牌](#csrf-considerations-multipart-url)

|   |在将 Spring Security 的 CSRF 保护与多部分文件上载集成在一起之前，请确保首先可以在没有 CSRF 保护的情况下进行上传。<br/>关于使用 Spring 的多部分表单的更多信息可以在 Spring 引用的[1.1.11.多部分旋转变压器](https://docs.spring.io/spring/docs/5.2.x/spring-framework-reference/web.html#mvc-multipart)部分和[MultipartFilter Javadoc](https://docs.spring.io/spring/docs/5.2.x/javadoc-api/org/springframework/web/multipart/support/MultipartFilter.html)中找到。|
|---| |

#### 将 CSRF 标记放入体内

第一个选项是在请求主体中包含实际的 CSRF 令牌。通过将 CSRF 令牌放置在主体中，主体将在执行授权之前被读取。这意味着任何人都可以在你的服务器上放置临时文件。但是，只有经过授权的用户才能提交由你的应用程序处理的文件。通常，这是推荐的方法，因为临时文件上传对大多数服务器的影响可以忽略不计。

#### 在 URL 中包含 CSRF 令牌

如果允许未经授权的用户上传临时文件是不可接受的，那么另一种选择是在表单的 Action 属性中包含预期的 CSRF 令牌作为查询参数。这种方法的缺点是查询参数可能会泄露。更普遍地说，将敏感数据放置在主体或标头内以确保其不会泄漏被认为是最佳实践。其他信息可以在[RFC2616 第 15.1.3 节在 URI 中对敏感信息进行编码](https://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3)中找到。

#### HiddenHttpMethodFilter

在某些应用程序中，表单参数可用于覆盖 HTTP 方法。例如，下面的表单可以用来将 HTTP 方法视为`delete`，而不是`post`。

例 10。CSRF 隐藏 HTTP 方法表单

```
<form action="/process"
	method="post">
	<!-- ... -->
	<input type="hidden"
		name="_method"
		value="delete"/>
</form>
```

在筛选器中重写 HTTP 方法。这个过滤器必须放在安全部门的支持之前。请注意，覆盖只发生在`post`上，因此这实际上不太可能导致任何实际问题。然而，最好的做法仍然是确保将其置于 Spring Security 的过滤器之前。