18. 跨站请求伪造(CSRF)

本节讨论 Spring Security 的跨站请求伪造(CSRF)支持。

18.1 CSRF 攻击

在讨论 Spring Security 如何保护应用程序免受 CSRF 攻击之前,我们将解释什么是 CSRF 攻击。让我们看一个具体的例子以获得更好的理解。

假设您的银行网站提供了一种表格,该表格允许将资金从当前登录的用户转移到另一个银行帐户。例如,HTTP 请求可能类似于:

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

amount=100.00&routingNumber=1234&account=9876

现在,Feign 您对银行的网站进行身份验证,然后在不注销的情况下访问一个邪恶的网站。恶意网站包含具有以下格式的 HTML 页面:

<form action="https://bank.example.com/transfer" method="post">
<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 可以使整个过程自动化。这意味着您甚至不需要单击按钮。那么,我们如何保护自己免受此类攻击呢?

18.2 同步器令牌模式

问题在于,来自银行网站的 HTTP 请求与来自邪恶网站的请求完全相同。这意味着无法拒绝来自邪恶网站的请求并允许来自银行网站的请求。为了防御 CSRF 攻击,我们需要确保恶意站点无法提供请求中的某些内容。

一种解决方案是使用同步器令牌模式。该解决方案是确保除我们的会话 cookie 之外,每个请求还需要随机生成的令牌作为 HTTP 参数。提交请求后,服务器必须查找参数的期望值,并将其与请求中的实际值进行比较。如果值不匹配,则请求应失败。

我们可以放宽期望,只要求每个更新状态的 HTTP 请求都需要令牌。可以安全地完成此操作,因为相同的来源策略可确保恶意站点无法读取响应。另外,我们不想在 HTTP GET 中包含随机令牌,因为这可能导致令牌泄漏。

让我们看一下示例将如何变化。假设在名为_csrf 的 HTTP 参数中存在随机生成的令牌。例如,转帐的请求如下所示:

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

amount=100.00&routingNumber=1234&account=9876&_csrf=<secure-random>

您会注意到,我们为_csrf 参数添加了一个随机值。现在邪恶网站将无法猜测_csrf 参数的正确值(必须在邪恶网站上明确提供),并且当服务器将实际令牌与预期令牌进行比较时,传输将失败。

18.3 何时使用 CSRF 保护

什么时候应该使用 CSRF 保护?我们的建议是对普通用户可能由浏览器处理的任何请求使用 CSRF 保护。如果仅创建非浏览器 Client 端使用的服务,则可能需要禁用 CSRF 保护。

18.3.1 CSRF 保护和 JSON

一个常见的问题是“我需要保护由 javascript 发出的 JSON 请求吗?”简短的答案是,这取决于。但是,您必须非常小心,因为有些 CSRF 漏洞会影响 JSON 请求。例如,恶意用户可以创建使用以下格式的带有 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 结构

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

如果应用程序未验证 Content-Type,则该应用程序将被暴露。根据设置的不同,仍然可以通过更新 URL 后缀以“ .json”结尾来利用可验证 Content Type 的 Spring MVC 应用程序,如下所示:

<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>

18.3.2 CSRF 和 Stateless 浏览器应用程序

如果我的应用程序是 Stateless 的怎么办?那并不一定意味着您受到保护。实际上,如果用户不需要针对给定请求在 Web 浏览器中执行任何操作,则他们可能仍然容易受到 CSRF 攻击。

例如,考虑一个应用程序使用一个包含其内所有状态的自定义 cookie 而不是 JSESSIONID 进行身份验证。进行 CSRF 攻击后,自定义 cookie 将与请求一起发送,其方式与在前面的示例中发送 JSESSIONID cookie 相同。

使用基本身份验证的用户也容易受到 CSRF 攻击,因为浏览器将在所有请求中自动包括用户名密码,就像在前面的示例中发送 JSESSIONID cookie 一样。

18.4 使用 Spring Security CSRF 保护

那么,使用 Spring Security 来保护我们的站点免受 CSRF 攻击的必要步骤是什么?下面概述了使用 Spring Security 的 CSRF 保护的步骤:

18.4.1 使用正确的 HTTP 动词

防御 CSRF 攻击的第一步是确保您的网站使用正确的 HTTP 动词。具体来说,在可以使用 Spring Security 的 CSRF 支持之前,您需要确定您的应用程序正在使用 PATCH,POST,PUT 和/或 DELETE 来修改状态。

这不是 Spring Security 支持的限制,而是对适当的 CSRF 预防的一般要求。原因是在 HTTP GET 中包含私人信息可能导致信息泄漏。有关敏感信息的使用 POST 而不是 GET 的一般指导,请参见RFC 2616 第 15.1.3 节在 URI 中编码敏感信息

18.4.2 配置 CSRF 保护

下一步是在您的应用程序中包括 Spring Security 的 CSRF 保护。一些框架通过使用户会话无效来处理无效的 CSRF 令牌,但这会导致自身的问题。相反,默认情况下,Spring Security 的 CSRF 保护将产生拒绝的 HTTP 403 访问。可以通过将AccessDeniedHandler配置为以不同方式处理InvalidCsrfTokenException进行自定义。

从 Spring Security 4.0 开始,默认情况下使用 XML 配置启用 CSRF 保护。如果要禁用 CSRF 保护,则可以在下面看到相应的 XML 配置。

<http>
	<!-- ... -->
	<csrf disabled="true"/>
</http>

默认情况下,使用 Java 配置会启用 CSRF 保护。如果您想禁用 CSRF,可以在下面看到相应的 Java 配置。有关如何配置 CSRF 保护的更多自定义信息,请参考 csrf()的 Javadoc。

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
	.csrf().disable();
}
}

18.4.3 包含 CSRF 令牌

Form Submissions

最后一步是确保在所有 PATCH,POST,PUT 和 DELETE 方法中都包含 CSRF 令牌。一种解决方法是使用_csrf request 属性获取当前的CsrfToken。下面显示了使用 JSP 进行此操作的示例:

<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
	method="post">
<input type="submit"
	value="Log out" />
<input type="hidden"
	name="${_csrf.parameterName}"
	value="${_csrf.token}"/>
</form>

一种更简单的方法是使用 Spring Security JSP 标签库中的csrfInput 标签

Note

如果您使用的是 Spring MVC <form:form>标记或Thymeleaf 2.1+并且使用@EnableWebSecurity,则会自动为您提供CsrfToken(使用CsrfRequestDataValueProcessor)。

Ajax 和 JSON 请求

如果您使用的是 JSON,则无法在 HTTP 参数内提交 CSRF 令牌。相反,您可以在 HTTP Headers 中提交令牌。一种典型的模式是在您的元标记中包含 CSRF 令牌。 JSP 的示例如下所示:

<html>
<head>
	<meta name="_csrf" content="${_csrf.token}"/>
	<!-- default header name is X-CSRF-TOKEN -->
	<meta name="_csrf_header" content="${_csrf.headerName}"/>
	<!-- ... -->
</head>
<!-- ... -->

您可以使用 Spring Security JSP 标签库中更简单的csrfMetaTags tag来代替手动创建 meta 标签。

然后,您可以将令牌包含在所有 Ajax 请求中。如果您使用的是 jQuery,则可以通过以下方式完成:

$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
	xhr.setRequestHeader(header, token);
});
});

作为 jQuery 的替代方法,我们建议使用cujoJS'srest.js。 rest.js模块为以 RESTful 方式处理 HTTP 请求和响应提供了高级支持。核心功能是通过将拦截器链接到 Client 端来根据需要上下文化 HTTPClient 端添加行为的功能。

var client = rest.chain(csrf, {
token: $("meta[name='_csrf']").attr("content"),
name: $("meta[name='_csrf_header']").attr("content")
});

可以与需要向 CSRF 保护的资源发出请求的应用程序的任何组件共享配置的 Client 端。 rest.js 和 jQuery 之间的一个重要区别是,仅使用配置的 Client 端发出的请求将包含 CSRF 令牌,而 jQuery 的所有请求将包括该令牌。范围限定请求接收令牌的能力有助于防止 CSRF 令牌泄漏给第三方。有关 rest.js 的更多信息,请参考rest.js 参考文档

CookieCsrfTokenRepository

在某些情况下,用户可能希望将CsrfToken保留在 cookie 中。默认情况下,CookieCsrfTokenRepository将写入名为XSRF-TOKEN的 cookie 并从名为X-XSRF-TOKEN的 Headers 或 HTTP 参数_csrf读取它。这些默认值来自AngularJS

您可以使用以下命令以 XML 配置CookieCsrfTokenRepository

<http>
	<!-- ... -->
	<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
	class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
	p:cookieHttpOnly="false"/>

Note

该示例显式设置cookieHttpOnly=false。这是允许 JavaScript(即 AngularJS)读取它所必需的。如果您不需要直接使用 JavaScript 读取 Cookie 的功能,建议省略cookieHttpOnly=false以提高安全性。

您可以使用以下命令在 Java 配置中配置CookieCsrfTokenRepository

@EnableWebSecurity
public class WebSecurityConfig extends
		WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.csrf()
				.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
	}
}

Note

该示例显式设置cookieHttpOnly=false。这是允许 JavaScript(即 AngularJS)读取它所必需的。如果您不需要直接使用 JavaScript 读取 Cookie 的功能,建议省略cookieHttpOnly=false(通过使用new CookieCsrfTokenRepository()来代替)以提高安全性。

18.5 CSRF 警告

实施 CSRF 时有一些警告。

18.5.1 Timeouts

一个问题是预期的 CSRF 令牌存储在 HttpSession 中,因此 HttpSession 过期后,您配置的AccessDeniedHandler将收到 InvalidCsrfTokenException。如果您使用默认的AccessDeniedHandler,则浏览器将获得 HTTP 403 并显示错误的错误消息。

Note

有人可能会问,默认情况下,为什么预期的CsrfToken没有存储在 Cookie 中。这是因为存在已知的利用方式,可以由另一个域设置 Headers(即指定 cookie)。这与 Ruby on Rails 当 Headers X-Requested-With 存在时,不再跳过 CSRF 检查的原因相同。有关如何执行此利用的详细信息,请参见此 webappsec.org 线程。另一个缺点是,通过删除状态(即超时),您将失去在令牌遭到破坏时强制终止令牌的能力。

缓解活动用户超时的一种简单方法是使用一些 JavaScript,该 JavaScript 可以使该用户知道其会话即将到期。用户可以单击一个按钮以 continue 并刷新会话。

另外,指定自定义AccessDeniedHandler可使您以自己喜欢的任何方式处理InvalidCsrfTokenException。有关如何自定义AccessDeniedHandler的示例,请参考提供的xmlJava configuration链接。

最后,可以将应用程序配置为使用CookieCsrfTokenRepository,它将不会过期。如前所述,这并不像使用会话那样安全,但是在许多情况下可以满足要求。

18.5.2 登录

为了防御伪造登录请求,登录表单也应受到防御 CSRF 攻击。由于CsrfToken存储在 HttpSession 中,因此这意味着在访问CsrfToken令牌属性后将立即创建 HttpSession。尽管这在 RESTful /Stateless 架构中听起来很糟糕,但现实是状态对于实现实际的安全性是必需的。没有状态,如果令牌被泄露,我们将无能为力。实际上,CSRF 令牌的大小非常小,对我们的架构的影响应该可以忽略不计。

保护表单登录的一种常用技术是在表单提交之前使用 JavaScript 函数获取有效的 CSRF 令牌。这样,就无需考虑会话超时(在上一节中讨论过),因为会话是在表单提交之前创建的(假设未配置CookieCsrfTokenRepository),因此用户可以停留在登录页面上并在需要时提交用户名/密码。为了实现这一点,您可以利用 Spring Security 提供的CsrfTokenArgumentResolver并公开一个端点,如here所述。

18.5.3 注销

添加 CSRF 会将 LogoutFilter 更新为仅使用 HTTP POST。这样可以确保注销需要 CSRF 令牌,并且恶意用户不能强制注销用户。

一种方法是使用表单进行注销。如果您确实想要一个链接,则可以使用 JavaScript 来使该链接执行 POST(即可能以隐藏形式)。对于禁用了 JavaScript 的浏览器,您可以选择使该链接将用户带到将执行 POST 的注销确认页面。

如果您确实想在注销时使用 HTTP GET,则可以这样做,但是请记住,通常不建议这样做。例如,以下 Java 配置将使用 URL 执行注销/使用任何 HTTP 方法请求注销:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.logout()
				.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
	}
}

18.5.4Multipart(文件上传)

将 CSRF 保护与 multipart/form-data 一起使用有两种选择。每个选项都有其权衡。

Note

在将 Spring Security 的 CSRF 保护与分段文件上传集成之前,请确保您可以首先在没有 CSRF 保护的情况下进行上传。有关在 Spring 上使用 Multipart 表单的更多信息,请参见 Spring 参考的17 .10 Spring 的 Multipart(文件上传)支持部分和MultipartFilter javadoc

在 Spring Security 之前放置 MultipartFilter

第一种选择是确保在 Spring Security 过滤器之前指定MultipartFilter。在 Spring Security 过滤器之前指定MultipartFilter意味着没有授权调用MultipartFilter,这意味着任何人都可以在您的服务器上放置临时文件。但是,只有授权用户才能提交由您的应用程序处理的文件。通常,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。

为了确保在使用 Java 配置的 Spring Security 过滤器之前指定了MultipartFilter,用户可以如下所示覆盖 beforeSpringSecurityFilterChain:

public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

	@Override
	protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
		insertFilters(servletContext, new MultipartFilter());
	}
}

为了确保在具有 XML 配置的 Spring Security 过滤器之前指定MultipartFilter,用户可以确保MultipartFilter的\ 元素位于 web.xml 中的 springSecurityFilterChain 之前,如下所示:

<filter>
	<filter-name>MultipartFilter</filter-name>
	<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
	<filter-name>springSecurityFilterChain</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
	<filter-name>MultipartFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
	<filter-name>springSecurityFilterChain</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

实际使用 CSRF 令牌

如果不允许未经授权的用户上传临时文件,则可以选择将MultipartFilter放在 Spring Security 过滤器之后,并将 CSRF 作为查询参数包括在表单的 action 属性中。带有 jsp 的示例如下所示

<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">

这种方法的缺点是查询参数可能会泄漏。一般而言,将敏感数据放置在正文或标题中以确保其不被泄露是最佳实践。其他信息可以在RFC 2616 第 15.1.3 节在 URI 中编码敏感信息中找到。

18.5.5 HiddenHttpMethodFilter

HiddenHttpMethodFilter 应该放在 Spring Security 过滤器之前。总的来说,这是正确的,但在防御 CSRF 攻击时可能会有其他含义。

请注意,HiddenHttpMethodFilter 仅覆盖 POST 上的 HTTP 方法,因此实际上不太可能引起任何实际问题。但是,仍然最好的方法是确保将其放置在 Spring Security 的过滤器之前。

18.6 覆盖默认值

Spring Security 的目标是提供默认值,以保护您的用户免遭攻击。这并不意味着您被迫接受其所有默认值。

例如,您可以提供一个自定义 CsrfTokenRepository 来覆盖CsrfToken的存储方式。

您还可以指定一个自定义的 RequestMatcher 来确定哪些请求受 CSRF 保护(即,您可能不在乎是否利用了注销)。简而言之,如果 Spring Security 的 CSRF 保护的行为不完全符合您的期望,则可以自定义行为。有关使用 XML 进行这些自定义的详细信息,请参见第 41.1.18 节“<csrf>”文档;有关使用 Java 配置时如何进行这些自定义的详细信息,请参考CsrfConfigurer javadoc。