19. 跨站请求伪造 (CSRF)

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

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

现在假装你认证你的银行网站,然后,没有登出,访问一个恶意的网站。恶意网站包含一个 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 自动化的。这意味着你甚至不需要点击这个按钮。那么我们如何保护自己免受这种攻击呢?

19.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 参数的正确值(必须在恶意网站上明确提供),并且当服务器将实际令牌与预期令牌进行比较时,传输将失败。

19.3 何时使用 CSRF 保护

什么时候使用 CSRF 保护?我们的建议是使用 CSRF 保护任何可以由浏览器处理正常用户的请求。如果你只创建非浏览器客户端使用的服务,那么你可能希望禁用 CSRF 保护。

19.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"
}

如果应用程序没有验证内容类型,那么它将暴露于此漏洞。根据设置,仍然可以通过更新 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>

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

如果我的应用程序是无状态的呢?这并不一定意味着你受到了保护。事实上,如果用户对于给定的请求不需要在 web 浏览器中执行任何操作,那么他们可能仍然容易受到 CSRF 攻击。

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

使用基本身份验证的用户也容易受到 CSRF 攻击,因为浏览器将以与前面示例中发送 JSESSIONID cookie 相同的方式在任何请求中自动包括用户名密码。

19.4 使用 Spring Security CSRF 保护

那么,使用 Spring Security 来保护我们的站点不受 CSRF 攻击的步骤是什么呢?使用 Spring Security 的 CSRF 保护的步骤概述如下:

  • 使用适当的 HTTP 动词
  • 配置 CSRF 保护
  • 包含 CSRF 令牌

19.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 中编码敏感信息。

19.4.2 配置 CSRF 保护

下一步是在应用程序中包含 Spring Security 的 CSRF 保护。有些框架通过无效用户会话来处理无效的 CSR F令牌,但这会导致其自身的问题。默认情况下,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();
	}
}

19.4.3 包含 CSRF 令牌

表单提交

最后一步是确保将 CSRF 令牌包含在所有 PATCH、POST、PUT 和 DELATE 方法中。解决这一问题的一种方法是使用 _csrf 请求属性来获得当前的 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] Note

如果你正在使用 Spring MVC <form:form> 标签或 Thymeleaf 2.1+,并且正在使用 @EnableWebSecurity,那么 CsrfToken 会自动为你包含(使用 CsrfRequestDataValueProcessor)。

Ajax 和 JSON 请求

如果使用 JSON,则不可能在 HTTP 参数中提交 CSRF 令牌。相反,你可以在 HTTP 报头中提交令牌。一个典型的模式是在你的元标签中包含 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 标签,而不是手动创建元标签。

然后可以在所有 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 的 rest.js。rest.js 模块提供了以 RESTful 方式处理 HTTP 请求和响应的高级支持。核心能力是根据需要通过将拦截器链接到客户端来,将 HTTP 客户端添加到行为的能力。

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

配置的客户端可以与需要向 CSRF 保护资源请求的应用程序的任何组件共享。rest.js 和 jQuery 之间的一个显著区别是,只有配置客户端的请求将包含 CSRF 令牌,相比 jQuery,所有请求都将包含令牌。请求接收令牌的范围的能力有助于防止将 CSRF 令牌泄漏给第三方。有关的更多信息信息请参阅 rest.js 参考文档。

CookieCsrfTokenRepository

有些情况下,用户希望在 cookie 中保留 CsrfToken。默认情况下,CookieCsrfTokenRepository 将写到名为 XSRF-TOKEN 的 cookie,并从名为 X-XSRF-TOKEN 的报头或 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] 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] Note

该示例显式设置 cookieHttpOnly=false。这是允许 JavaScript(即 AngularJS)读取它的必要条件。如果不需要使用 JavaScript 直接读取 cookie 的能力,建议省略 cookieHttpOnly=false(改为使用新的 new CookieCsrfTokenRepository() )来提高安全性。

19.5 CSRF 注意事项

在实现 CSRF 时有一些注意事项。

19.5.1 超时

一个问题是,预期的 CSRF 令牌存储在 HttpSession 中,所以一旦 HttpSession 期满,配置的 AccessDeniedHandler 将收到 InvalidCsrfTokenException。如果你使用默认的 AccessDeniedHandler,浏览器将获得 HTTP 403 并显示低劣的错误消息。

[Note] Note

人们可能会问,为什么默认的 CsrfToken 默认不存储在 cookie 中。这是因为有已知的漏洞,其中报头(即指定 cookies)可以由另一个域来设置。这也是 Ruby on Rails 当 X-Requested-With 在时不再跳过 CSRF 检查的原因。有关如何执行漏洞的详细信息,请参阅 webappsec.org 线程。另一个缺点是,通过删除状态(即超时),如果令牌被破坏,你将失去强制终止令牌的能力。

减轻活动用户超时的简单方法是使用一些J avaScript,让用户知道他们的会话即将到期。用户可以单击一个按钮来继续并刷新会话。

或者,指定自定义 AccessDeniedHandler 允许你以任何你喜欢的方式处理 InvalidCsrfTokenException。有关如何自定义 AccessDeniedHandler 的示例,请参阅所提供的 xml 和 Java 配置的链接。

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

19.5.2 登录

为了防止伪造登录请求,登录表单也应该受到保护,防止 CSRF 攻击。由于 CsrfToken 存储在 HttpSession 中,这意味着一旦访问 CsrfToken 令牌属性,就会创建 HttpSession。虽然在 RESTful / 无状态架构中这听起来很糟糕,但实际情况是状态对实现实际安全性是必要的。没有状态,我们没有什么可以做,如果一个令牌妥协。实际上,CSRF 令牌的规模很小,对我们的架构应该没有不可忽视的影响。

保护表单登录的常用技术是使用 JavaScript 函数在表单提交之前获得有效的 CSRF 令牌。通过这样做,不需要考虑会话超时(在上一节中讨论),因为会话是在表单提交之前创建的(假设没有配置 CookieCsrfTokenRepository),所以用户可以停留在登录页上并当他想要的时候提交用户名/密码,为了实现这一点,你可以利用 Spring Security 提供的 CsrfTokenArgumentResolver 并公开一个端点,就像这里描述的那样。

19.5.3 注销

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

一种方法是使用表单注销。如果你真的想要一个链接,你可以使用 JavaScript 让链接执行一个 POST(也就是说,在一个隐藏的表单上)。对于禁用 JavaScript 的浏览器,可以选择让链接将用户带到将执行 POST 的注销确认页面。

如果你真的想使用注销的 HTTP GET,你可以这样做,但是记住这是不推荐的。例如,下面的 Java 配置将执行注销,URL /logout 是用任何 HTTP 方法请求的:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

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

19.5.4 Multipart(文件上传)

使用 multipart/form-data 时使用 CSRF 保护有两种选择。每个选项都有它的折衷。

  • 在 Spring Security 之前放置 MultipartFilter
  • 在动作中包括 CSRF 令牌
[Note] Note

在将 Spring Security 的 CSRF 保护与 multipart 文件上传集成之前,请确保可以先上传而不需要 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 的 <filter-mapping> 元素放置在 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 中敏感信息的编码。

19.5.5 HiddenHttpMethodFilter

应该在 Spring Security 过滤器之前放置 HiddenHttpMethodFilter。一般来说,这是正确的,但是它可以在保护 CSRF 攻击时具有额外的含义。

注意,HiddenHttpMethodFilter 只覆盖 POST 上的 HTTP 方法,因此这实际上不太可能导致任何实际问题。然而,它仍然是最好的做法,以确保它被放置在 Spring Security 的过滤器之前。

19.6 重写默认值

Spring Security 的目标是提供保护用户免受漏洞攻击的默认值。这并不意味着你被迫接受它的所有默认值。

例如,可以提供自定义的 CsrfTokenRepository 来重写存储 CsrfToken 的方式。

还可以指定自定义 RequestMatcher 来确定哪些请求受 CSRF 保护(即,也许你不关心是否利用了注销)。简而言之,如果 Spring Security 的 CSRF 保护不完全符合你的要求,就可以自定义行为。有关如何使用 XML 请参阅 小节 43.1.19, “<csrf>” 文档,以及了解如何在使用 Java 配置时进行这些自定义的细节请参阅 CsrfConfigurer javadoc。

20. CORS

Spring Framework 为 CORS 提供了一流的支持。CORS 必须在 Spring Security 之前进行处理,因为预检请求将不包含任何 cookies(即 JSESSIONID)。如果请求不包含任何 cookie,并且 Spring Security 是第一个,则请求将确定用户没有经过身份验证(因为请求中没有 cookie)并拒绝它。

确保 CORS 首先处理的最简单的方法是使用 CorsFilter。用户可以通过使用以下配置提供 CorsConfigurationSource 来将 CorsFilter 与 Spring Security 集成:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			// by default uses a Bean by the name of corsConfigurationSource
			.cors().and()
			...
	}

	@Bean
	CorsConfigurationSource corsConfigurationSource() {
		CorsConfiguration configuration = new CorsConfiguration();
		configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
		configuration.setAllowedMethods(Arrays.asList("GET","POST"));
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration("/**", configuration);
		return source;
	}
}

或者用 XML

<http>
	<cors configuration-source-ref="corsSource"/>
	...
</http>
<b:bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
	...
</b:bean>

如果使用 Spring MVC 的 CORS 支持,可以省略指定 CorsConfigurationSource,Spring Security 将利用提供给 Spring MVC 的 CORS 配置。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			// if Spring MVC is on classpath and no CorsConfigurationSource is provided,
			// Spring Security will use CORS configuration provided to Spring MVC
			.cors().and()
			...
	}
}

或者用 XML

<http>
	<!-- Default to Spring MVC's CORS configuration -->
	<cors />
	...
</http>