1.什么是CSRF攻击
下面我们以一个具体的例子来说明这种常见的攻击模式
1.1 假定某个银行的网站提供让当前登录用户给其他账号转账的功能,转账请求的格式如下
1.2 现在有一个用户登录到了这个银行的网站并且进行了认证,在未logout的情况下访问了一个恶意网站,恶意网站包含如下html代码段
这时如果用户点击了【领取奖品】按钮,你将给那个恶意攻击者的账户转入100元,这是因为虽然恶意网站不能获取到用户cookie信息,但是当点击【领取奖品】按钮访问银行网站时,cookie信息还是会一起发送。更糟糕的是,借助于javascript,上面的过程可以自动执行,不需要等待用户点击【领取奖品】按钮,只要你打开了这个页面,你的钱就被偷走了,这也是我们常见的钓鱼网站的做法。
[b]像这样虽然用户访问的是另一个网站,但是这个网站却伪装成当前认证用户访问用户正在访问的网站以实现攻击的方式称为CSRF。[/b]
2.同步token模式
2.1 从上面的例子可以看出,无论转账请求是从银行自己的网站发出,还是从恶意网站发出,对于银行的服务端来说内容是一样的,所以单纯从服务端来讲我们没法过滤掉那些恶意的请求。如果我们能采取一种措施让银行的正常页面发请求时给服务器提供一个凭证,并且这个凭证是恶意网站所不能提供的,这样服务器端就可以很容器拒绝掉那些非法的请求了。
同步token就是这样的一种方式,他要求在客户端发起请求时,除了cookie信息外,还需要提供一个随机的token值作为参数。当服务器收到一个请求后,会先解析出这个token值,再和期望的值进行比较,如果不匹配则拒绝提供服务。
2.2 在实际项目中,我们可以放宽上面的规则,只要求那些会修改信息的请求才提供token值,因为根据同源策略,那些恶意网站是不能获取到我们正常请求的响应结果的。追加完token后,我们的请求示例如下:
这样,因为恶意网站不能狗提供_csrf对应的随机值,伪造的请求将不会被服务器接受。
3.如何利用spring security防止csrf攻击
3.1 采用正确的http动词
[table]
|动词| 作用 | 类比数据库操作
|GET|从服务器获取信息|select
|POST|在服务器上新创建一个资源|insert
|PUT|更新服务器上的一个资源,本次请求包含完整的信息| update
|PATCH|更新服务器上的一个资源,包含部分信息| update
|DELETE|删除服务器上的一个资源| delete
|HEAD|向服务器索要与GET请求相一致的响应,只不过只有头部信息,响应体将不会被返回。| 无
|TRACE|回显服务器收到的请求,主要用于测试或诊断| 无
|OPTIONS|返回服务器针对特定资源所支持的HTTP请求方法| 无
[/table]
确保对信息进行修改的请求其动词一定是post、put、patch、delete中的一种。
3.2配置CSRF
对于spring security4.0+无论是xml配置形式还是Java config形式,csrf默认都是开启的,可以通过如下方式关闭
默认情况下,如果csrf检查失败,spring security会返回客户端一个403码,可以通过http.csrf()方法返回的CsrfConfigurer类来定制AccessDeniedHandler,实现自己的逻辑。
3.3界面包含CSRF相关设定
我们要确保在所有执行post、put、patch、delete的请求中包含CSRF token值,最直接的方式是使jstl表达式从request中获取到_csrf对应的值,如下
另外spring 也为我们提供了两个方便的jsptag。具体例子参考[url=https://github.com/fengyilin/spring-security-sample/tree/master/security-samples-javaconfig-csrf-jsptag]csrf jsptag[/url]
另外如果我们采用thymeleaf模版引擎,也会自动的为form表单追加对应的csrf属性。
具体例子参考[url url=https://github.com/fengyilin/spring-security-sample/tree/master/security-samples-javaconfig-csrf-thymeleaf]csrf-thymeleaf[/url]
3.4 CookieCsrfTokenRepository
在某些场合下,我们可能会需要将csrf token值存储在cookie中,此时可以用CookieCsrfTokenRepository来实现这个功能,默认情况下,写入到cookie中的key是XSRF-TOKEN,读取时从request header的X-XSRF-TOKEN中或者parameter的_csrf中读取。可以用如下代码段配置
默认情况下设置到cookie中的信息不能够通过js读取,如果需要js访问的话需要明确设定
4 使用csrf的一些注意点
4.1 Timeouts
默认情况下,csrf token存储在httpsession中,当session过期时AccessDeniedHandler会收到一个InvalidCsrfTokenException,如果我们使用的是spring security默认的AccessDeniedHandler,客户端将会收到一个简单的403错误(因为 CSRF_FILTER在认证filter的后面),对于这个问题,我们可以采用下面几种方式处理
[list]
[*]在页面里面追加js,当session快过期时通知用户,可以通过用户点击按钮的方式刷新session
[*]自定义AccessDeniedHandle,自己处理InvalidCsrfTokenException
[*]也可以将csrf token存入CsrfTokenRepository中(如CookieCsrfTokenRepository),这样就不会有过期问题,虽然存入token中有安全隐患,不过大多数情况下都不会有问题
[/list]
4.2 登录问题
如果在登录页面也启用csrf token保护,就需要在登录前生成CsrfToken时创建HttpSession,这时就需要考虑如果用户在登录页面长时间停留,会引起session过期问题,当登录时直接返回403(没权限登录--),现在通用的解决方法是采用JavaScript在点击登录时,先获取token值,接着在提交登录请求,这样session在登录时才创建,用户就可以在登录界面停留任意时间了,利用CsrfTokenArgumentResolver我们很容易实现这样的功能
此时需要注意,为了安全不能把cors功能应用到这个endpoint上。
4.3 Logout
默认情况下,启用csrf token后,LogoutFilter只接收Post请求,并且logout时还需要提供csrf token值
如果Logout操作安全性没有那么高,实现时不想这么复杂,可以通过下面代码段配置
这时可以用任意的HTTP method执行logout操作
4.4 文件上传
可通过以下两种方式解决
[*]将MultipartFilter放到spring security 相关filter前面
[*]在上传文件对应的action中追加token值
下面我们以一个具体的例子来说明这种常见的攻击模式
1.1 假定某个银行的网站提供让当前登录用户给其他账号转账的功能,转账请求的格式如下
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
1.2 现在有一个用户登录到了这个银行的网站并且进行了认证,在未logout的情况下访问了一个恶意网站,恶意网站包含如下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="领取奖品"/>
</form>
这时如果用户点击了【领取奖品】按钮,你将给那个恶意攻击者的账户转入100元,这是因为虽然恶意网站不能获取到用户cookie信息,但是当点击【领取奖品】按钮访问银行网站时,cookie信息还是会一起发送。更糟糕的是,借助于javascript,上面的过程可以自动执行,不需要等待用户点击【领取奖品】按钮,只要你打开了这个页面,你的钱就被偷走了,这也是我们常见的钓鱼网站的做法。
[b]像这样虽然用户访问的是另一个网站,但是这个网站却伪装成当前认证用户访问用户正在访问的网站以实现攻击的方式称为CSRF。[/b]
2.同步token模式
2.1 从上面的例子可以看出,无论转账请求是从银行自己的网站发出,还是从恶意网站发出,对于银行的服务端来说内容是一样的,所以单纯从服务端来讲我们没法过滤掉那些恶意的请求。如果我们能采取一种措施让银行的正常页面发请求时给服务器提供一个凭证,并且这个凭证是恶意网站所不能提供的,这样服务器端就可以很容器拒绝掉那些非法的请求了。
同步token就是这样的一种方式,他要求在客户端发起请求时,除了cookie信息外,还需要提供一个随机的token值作为参数。当服务器收到一个请求后,会先解析出这个token值,再和期望的值进行比较,如果不匹配则拒绝提供服务。
2.2 在实际项目中,我们可以放宽上面的规则,只要求那些会修改信息的请求才提供token值,因为根据同源策略,那些恶意网站是不能获取到我们正常请求的响应结果的。追加完token后,我们的请求示例如下:
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对应的随机值,伪造的请求将不会被服务器接受。
3.如何利用spring security防止csrf攻击
3.1 采用正确的http动词
[table]
|动词| 作用 | 类比数据库操作
|GET|从服务器获取信息|select
|POST|在服务器上新创建一个资源|insert
|PUT|更新服务器上的一个资源,本次请求包含完整的信息| update
|PATCH|更新服务器上的一个资源,包含部分信息| update
|DELETE|删除服务器上的一个资源| delete
|HEAD|向服务器索要与GET请求相一致的响应,只不过只有头部信息,响应体将不会被返回。| 无
|TRACE|回显服务器收到的请求,主要用于测试或诊断| 无
|OPTIONS|返回服务器针对特定资源所支持的HTTP请求方法| 无
[/table]
确保对信息进行修改的请求其动词一定是post、put、patch、delete中的一种。
3.2配置CSRF
对于spring security4.0+无论是xml配置形式还是Java config形式,csrf默认都是开启的,可以通过如下方式关闭
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable();
}
}
默认情况下,如果csrf检查失败,spring security会返回客户端一个403码,可以通过http.csrf()方法返回的CsrfConfigurer类来定制AccessDeniedHandler,实现自己的逻辑。
3.3界面包含CSRF相关设定
我们要确保在所有执行post、put、patch、delete的请求中包含CSRF token值,最直接的方式是使jstl表达式从request中获取到_csrf对应的值,如下
<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 也为我们提供了两个方便的jsptag。具体例子参考[url=https://github.com/fengyilin/spring-security-sample/tree/master/security-samples-javaconfig-csrf-jsptag]csrf jsptag[/url]
另外如果我们采用thymeleaf模版引擎,也会自动的为form表单追加对应的csrf属性。
具体例子参考[url url=https://github.com/fengyilin/spring-security-sample/tree/master/security-samples-javaconfig-csrf-thymeleaf]csrf-thymeleaf[/url]
3.4 CookieCsrfTokenRepository
在某些场合下,我们可能会需要将csrf token值存储在cookie中,此时可以用CookieCsrfTokenRepository来实现这个功能,默认情况下,写入到cookie中的key是XSRF-TOKEN,读取时从request header的X-XSRF-TOKEN中或者parameter的_csrf中读取。可以用如下代码段配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(new CookieCsrfTokenRepository());
}
}
默认情况下设置到cookie中的信息不能够通过js读取,如果需要js访问的话需要明确设定
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
4 使用csrf的一些注意点
4.1 Timeouts
默认情况下,csrf token存储在httpsession中,当session过期时AccessDeniedHandler会收到一个InvalidCsrfTokenException,如果我们使用的是spring security默认的AccessDeniedHandler,客户端将会收到一个简单的403错误(因为 CSRF_FILTER在认证filter的后面),对于这个问题,我们可以采用下面几种方式处理
[list]
[*]在页面里面追加js,当session快过期时通知用户,可以通过用户点击按钮的方式刷新session
[*]自定义AccessDeniedHandle,自己处理InvalidCsrfTokenException
[*]也可以将csrf token存入CsrfTokenRepository中(如CookieCsrfTokenRepository),这样就不会有过期问题,虽然存入token中有安全隐患,不过大多数情况下都不会有问题
[/list]
4.2 登录问题
如果在登录页面也启用csrf token保护,就需要在登录前生成CsrfToken时创建HttpSession,这时就需要考虑如果用户在登录页面长时间停留,会引起session过期问题,当登录时直接返回403(没权限登录--),现在通用的解决方法是采用JavaScript在点击登录时,先获取token值,接着在提交登录请求,这样session在登录时才创建,用户就可以在登录界面停留任意时间了,利用CsrfTokenArgumentResolver我们很容易实现这样的功能
@RestController
public class CsrfController {
@RequestMapping("/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}
此时需要注意,为了安全不能把cors功能应用到这个endpoint上。
4.3 Logout
默认情况下,启用csrf token后,LogoutFilter只接收Post请求,并且logout时还需要提供csrf token值
@SuppressWarnings("unchecked")
private RequestMatcher getLogoutRequestMatcher(H http) {
if (logoutRequestMatcher != null) {
return logoutRequestMatcher;
}
if (http.getConfigurer(CsrfConfigurer.class) != null) {
this.logoutRequestMatcher = new AntPathRequestMatcher(this.logoutUrl, "POST");
}
else {
this.logoutRequestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(this.logoutUrl, "GET"),
new AntPathRequestMatcher(this.logoutUrl, "POST"),
new AntPathRequestMatcher(this.logoutUrl, "PUT"),
new AntPathRequestMatcher(this.logoutUrl, "DELETE")
);
}
return this.logoutRequestMatcher;
}
如果Logout操作安全性没有那么高,实现时不想这么复杂,可以通过下面代码段配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
}
}
这时可以用任意的HTTP method执行logout操作
4.4 文件上传
可通过以下两种方式解决
[*]将MultipartFilter放到spring security 相关filter前面
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}
[*]在上传文件对应的action中追加token值
<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form- data">