10.6.1 CSRF Attacks
在讨论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实现自动化。这意味着你甚至不需要点击按钮。那么,我们如何保护自己免受这种攻击呢?
10.6.2 Synchronizer Token Pattern
问题是,来自银行网站的HTTP请求和来自邪恶网站的请求完全相同。这意味着无法拒绝来自邪恶网站的请求只允许来自银行网站的请求。为了防止CSRF攻击,我们需要确保邪恶站点无法提供请求中的某些内容。
一种解决方案是使用 Synchronizer Token Pattern。这个解决方案是为了确保除了会话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参数的正确值(必须在邪恶网站上明确提供),当服务器将实际令牌与预期令牌进行比较时,传输将失败。
10.6.3 When to use CSRF protection
什么时候应该使用CSRF保护?我们的建议是对正常用户可以通过浏览器处理的任何请求使用CSRF保护。如果您只创建一个非浏览器客户端使用的服务,那么您可能希望禁用CSRF保护。
CSRF protection and JSON
一个常见的问题是,“我需要保护JavaScript发出的JSON请求吗?”简而言之,这要看情况而定。但是,您必须非常小心,因为存在可能影响JSON请求的CSRF漏洞。例如,恶意用户可以使用以下表单使用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,那么它将暴露于此漏洞中。根据设置的不同,验证 Content-Type 的SpringMVC应用程序仍然可以通过更新url后缀以“.json”结尾来利用,如下所示:
<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 and Stateless Browser Applications
如果我的应用程序是无状态的呢?这并不一定意味着你受到了保护。事实上,如果用户对于给定的请求不需要在Web浏览器中执行任何操作,那么他们可能仍然容易受到CSRF攻击。
例如,假设一个应用程序使用一个自定义cookie,该cookie包含它内部的所有状态来进行身份验证,而不是JSessionID。当进行CSRF攻击时,定制cookie将与请求一起发送,发送方式与上一个示例中发送JSessionID cookie的方式相同。
使用 basic 认证的用户也容易受到CSRF攻击,因为浏览器将自动在任何请求中包含用户名密码,其方式与我们上一个示例中发送JSessionID cookie的方式相同。
10.6.4 Using Spring Security CSRF Protection
那么,使用Spring Security保护我们的站点免受CSRF攻击所必需的步骤是什么?使用Spring Security的CSRF保护的步骤概述如下:
Use proper HTTP verbs 使用正确的HTTP动词
防止CSRF攻击的第一步是确保您的网站使用正确的HTTP动词。具体来说,在Spring Security的CSRF支持可以使用之前,您需要确保您的应用程序正在使用 PATCH、POST、PUT和/或 DELETE 来修改状态。
这不是Spring Security支持的限制,而是正确预防CSRF的一般要求。原因是在HTTP GET中包含私有信息会导致信息泄漏。请参阅 RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s ,以获取有关使用POST而不是GET获取敏感信息的一般指导。
Configure CSRF Protection 配置CSRF保护
下一步是在你的应用程序中引入Spring Security 的CSRF保护。一些框架通过使用户的会话无效来处理无效的CSRF令牌,但这会导致自身的问题。相反,默认情况下,Spring Security的CSRF保护将导致HTTP 403访问被拒绝。这可以通过配置 AccessDeniedHandler 以不同方式处理 InvalidCsrfTokenException 来定制。
从Spring Security 4.0开始,CSRF保护默认通过XML配置启用。如果要禁用CSRF保护,可以在下面看到相应的XML配置。
<http>
<!-- ... -->
<csrfdisabled="true"/>
</http>
CSRF保护在使用Java配置时默认启用。如果您想禁用CSRF,可以看下面相应的Java配置。有关CSRF保护的其他自定义配置,请参阅csrf()的javadoc。
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable();
}
}
Include the CSRF Token
Form Submissions 表单提交
最后一步是确保在所有 PATCH, POST, PUT 和 DELETE 方法中都包含CSRF令牌。一种方法是使用 _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>
一种更简单的方法是使用SpringSecurityJSP标记库中的 the csrfInput tag 标记。
如果您使用的是spring mvc<form:form> tag或 Thymeleaf 2.1+,并且使用的是 @enableWebSecurity,则会自动为你包含 CsrfToken (使用 CsrfRequestDataValueProcessor )。
Ajax and JSON Requests
如果你正在使用JSON,那么就不可能在HTTP参数中提交CSRF令牌。相反,您可以在HTTP头中提交令牌。典型的模式是在 <meta> 中包含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的rest.js。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的更多信息,请参阅rest.js参考文档。
CookieCsrfTokenRepository
在某些情况下,用户可能希望在cookie中保留 CsrfToken 。默认情况下,CookieCsrfTokenRepository 将在cookie中写入名为 XSRF-TOKEN 的 token,并从名为 X-XSRF-TOKEN 的头或http参数 _csrf 读取该 token。这些默认值来自AngularJS
可以使用以下方法在XML中配置 CookieCsrfTokenRepository :
<http>
<!-- ... -->
<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
p:cookieHttpOnly="false"/>
示例显式设置 cookieHttpOnly=false。这是允许JavaScript(即AngularJS)读取它所必需的。如果不需要直接使用javascript读取cookie,建议省略 cookieHttpOnly=false 以提高安全性。
10.6.5 CSRF Caveats
在实施CSRF时有一些注意事项。
Timeouts
一个问题是预期的CSRF令牌存储在 HttpSession 中,因此一旦 HttpSession 过期,配置的 AccessDeniedHandler 将收到一个无效的 InvalidCsrfTokenException。如果您使用的是默认的 AccessDeniedHandler,浏览器将得到一个HTTP403并显示一条错误消息。
有人可能会问,为什么默认情况下预期的 CsrfToken 没有存储在cookie中。这是因为存在已知的漏洞,其中头(即指定cookie)可以由另一个域设置。这也是RubyonRails在存在头X-requested-with时不再跳过CSRF检查的原因。有关如何执行该漏洞攻击的详细信息,请参阅此webappsec.org线程。另一个缺点是,通过删除状态(即超时),您将失去在令牌受到威胁时强制终止令牌的能力。
缓解活动用户超时的一个简单方法是使用一些JavaScript让用户知道他们的会话即将到期。用户可以单击按钮继续并刷新会话。
或者,指定一个自定义的accessdeniedHandler允许您以任何方式处理invalidCSRFtokenException。有关如何自定义AccessDeniedHandler 的示例,请参阅所提供的XML和Java配置的链接。
最后,可以将应用程序配置为使用不会过期的 CookicsrftokenRepository。如前所述,这并不像使用会话那样安全,但在许多情况下,这已经足够好了。
Logging In
为了防止伪造登录请求,登录表单也应防止CSRF攻击。由于 CsrfToken 存储在 HttpSession 中,这意味着一旦访问CsrfToken 属性,就会创建 HttpSession 。虽然这在一个RESTful/无状态的体系结构中听起来很糟糕,但事实是状态对于实现真正的安全是必要的。没有状态,一旦token被破坏我们就无能为力。实际上,CSRF令牌的大小非常小,对我们的体系结构的影响应该可以忽略不计。
保护登录表单的常见技术是在表单提交之前使用javascript函数获取有效的CSRF令牌。通过这样做,无需考虑会话超时(在上一节中讨论),因为会话是在表单提交之前创建的(假设没有配置 CookieCsrfTokenRepository),因此用户可以留在登录页面上,并在需要时提交用户名/密码。为了实现这一点,您可以利用Spring Security 提供的CsrfTokenArgumentResolver ,并像这里描述的那样公开一个端点。
Logging Out
启动了CSRF,将更新 LogoutFilter 只能使用 HTTP POST(不启用CSRF时,GET请求也是可以正常退出的,因为其不需要CSRF token)。这可以确保注销需要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"));
}
}
Multipart (file upload)
对 multipart/form-data 使用CSRF保护有两个选项。每个选项都有其权衡。
Placing MultipartFilter before Spring Security
在将Spring Security的CSRF保护与多部分文件上传集成之前,请确保您可以先上传而不需要CSRF保护。有关将多部分表单与Spring一起使用的更多信息,可以在 17.10 Spring’s multipart (file upload) support 章节和 MultipartFilter javadoc 中找到。
Placing MultipartFilter before Spring Security
第一个选项是确保在Spring Security 过滤器之前指定 MultipartFilter 。在Spring安全过滤器之前指定 MultipartFilter 意味着没有调用 MultipartFilter 的授权,这意味着任何人都可以在服务器上放置临时文件。但是,只有授权用户才能提交由您的应用程序处理的文件。一般来说,这是推荐的方法,因为临时文件上载对大多数服务器都会产生不可忽略的影响。
为了确保在使用Java配置的Spring Security 过滤器之前指定 MultipartFilter ,用户可以覆盖beforeSpringSecurityFilterChain ,如下所示:
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}
为了确保 MultipartFilter 在使用xml配置的spring Security 过滤器之前被指定,用户可以确保 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>
Include CSRF token in action
如果不允许未经授权的用户上传临时文件,则可以将 MultipartFilter 放在spring Security 过滤器之后,并将 CSRF 作为查询参数包含在表单的action属性中。下面是一个带有JSP的示例
<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">
这种方法的缺点是可以泄漏查询参数。一般来说,最好的做法是将敏感数据放在主体或头中,以确保不会泄漏。其他信息可在 RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s.中查找。
HiddenHttpMethodFilter
HiddenHttpMethodFilter 应该放在Spring Security过滤器之前。一般来说,这是正确的,但在防止CSRF攻击时,它可能会有额外的影响。
请注意,HiddenHttpMethodFilter 只重写HTTP 的POST方法,因此这实际上不太可能导致任何实际问题。但是,最好还是将其放在Spring Security过滤器之前。
10.6.6 Overriding Defaults
SpringSecurity的目标是提供保护您的用户免受攻击的默认值。这并不意味着您必须接受它的所有默认值。
例如,您可以提供一个自定义的 CsrfTokenRepository 来覆盖 CSRFtoken 的存储方式。为了保证在分布式系统中可以使用,用Redis来存储是比较常见的实现方式。
您还可以指定一个自定义的 RequestMatcher 来确定哪些请求受CSRF保护(也就是说,您可能不关心logout是否安全)。简而言之,如果Spring Security的CSRF保护行为不完全符合您的要求,那么您可以自定义该行为。有关如何使用XML和java配置来进行这些自定义的详细信息,详见the section called “<csrf>”章节。