Spring Security 中的 CSRF(Cross-Site Request Forgery)攻击防御是非常重要的安全机制。在这个攻防机制中,Spring Security 通过在表单中添加或者 TkCooikeToken 的形式防御 CSRF 攻击,保障 Web 应用程序的安全。
CSRF 防御机制
CSRF 攻击利用用户在 Web 浏览器中处于登录状态的 Cookie 值,来发起伪造请求从而获取资源或者执行关键操作。Spring Security 通过如下两种方式来防御 CSRF 攻击:
-
添加 CSRF Token 到每个表单中:我们可以将 CSRF Token 添加到每个表单中,这样表单提交时就会同时提交这个 Token 和表单数据,并且服务器会判断这个 Token 是否有效,从而确认该请求是否合法。
-
利用 HttpOnly Cookie 和 Header:同时,我们也可以使用 HttpOnly Cookie,它会指定 Cookie 只能通过 Http 协议传输(即不能使用 JavaScript 访问),以及设置 CSRF Token 在 http response header 中,避免 CSRF 攻击者伪造 Http 请求。
添加 CSRF Token 到每个表单中
Spring Security 的 CSRF 保护默认是开启的,可以通过下面的配置控制是否需要开启保护:
spring.security.enabled: true
默认情况下,Spring Security 在渲染表单时,会自动在表单的隐藏字段(名称为 _csrf
)中添加一个 Token:
<input type="hidden" name="_csrf" value="c6cac604-4c3f-47f2-8a20-c4471260bfe1">
同时,Spring Security 会在服务器端拦截请求,判断请求中的 _csrf
值是否和当前 Session 中的 _csrf
值一致,如果不一致,则视为 CSRF 攻击,请求被拒绝。
需要注意的是,在使用 Thymeleaf 模板引擎时,Spring Security 提供了一个方便的标签来生成 CSRF Token,即 th:csrf
,示例如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Blog</title>
</head>
<body>
<form method="post" th:action="@{/create}">
<input type="text" name="title" />
<textarea name="content"></textarea>
<button type="submit">Submit</button>
<!-- 表单中添加了th:csrf标签 -->
<input type="hidden" th:csrf="${_csrf.token}" />
</form>
</body>
</html>
利用 HttpOnly Cookie 和 Header
除了在表单中添加 CSRF Token 外,Spring Security 还支持利用 HttpOnly Cookie 和 Header 来防御 CSRF 攻击。
- HttpOnly Cookie
可以通过以下的配置为 CSRF Token 设置 HttpOnly Cookie:
spring.security.csrf.cookie.httpOnly: true
这样,Spring Security 会将 CSRF Token 放入到 HttpOnly Cookie 中,浏览器不能通过 JavaScript 访问此 Cookie,从而有效防止 CSRF 攻击者产生伪造请求。
可以通过如下的方式检查 HttpOnly Cookie:
HTTP/1.1 200 OK
Set-Cookie: csrf_token=ZJLWV3DGJA3L3GZH5A47BO54PABELPFE; Path=/; HttpOnly
- Header
可以通过以下的配置将 CSRF Token 作为 Header 发送到客户端:
spring.security.csrf.headerName:X-XSRF-TOKEN
客户端需要将获取到的 Token 在之后的请求头中发送到服务器,示例如下:
POST /api/saveUser HTTP/1.1
Host: example.com
X-XSRF-TOKEN: zWfB3yGnNTLuG98Vibpv
Content-Type: application/json
...
在服务器端,Spring Security 会解析 Header 中的 CSRF Token,与当前 Session 中的 CSRF Token 进行比较,如果不一致,则视为 CSRF 攻击,请求被拒绝。
示例1
原网站中含有一个表单页面,表单需要用户输入邮箱、密码信息,完成操作后登录。
恶意攻击者制作出一个 CSFR 攻击页面,页面中含有一个表单,表单中有两个隐藏的 input 输入框:邮箱、密码,这个表单即使用户没有登录到被攻击页面中,也可以正常提交数据。
攻击者送给用户一个欺骗性的 URL 地址,让用户误以为是正常用户登陆页面的地址,用户打开这个欺骗地址,就会看到一个假的登录页面;当表单中被提交的时候,被攻击者页面的邮箱和密码都被提交到他所指向的真正登录页面上,用户完成操作后就成为了攻击者的虚假账户。
防御机制:在表单中添加 CSRF Token。
假设网站服务器端是采用java spring boot框架进行开发的,那么需要使用类似于下面的代码,在后台使用Thymeleaf模板渲染数据并渲染CSRF Token
<form action="/login" method="post">
<input type="text" name="email" placeholder="请输入邮箱" />
<input type="password" name="password" placeholder="请输入密码" />
<!-- 添加CSRF Token标签,名称为字段为 _csrf -->
<input type="hidden" name="_csrf" value="${_csrf.token}"/>
<button type="submit">登录</button>
</form>
示例2
当Web页面中有图片资源需要加载(通常是从一个不同的域加载),加载域的服务器里面存在漏洞,导致漏洞被攻击者利用,而这个图片会要求跨域读取一些需要保护的资源,这时候会涉及到CSRF攻击。
防御机制:使用 HttpOnly Cookie 和 Header。
假设服务器端是采用java spring boot框架的话,可以通过在 Security 配置文件中添加如下代码启用 HttpOnly Cookie 和 Header 防御 CSRF 攻击:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().ignoringAntMatchers("/api/**")
.and()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
.addFilterAfter(new CustomCsrfFilter(), CsrfFilter.class)
.authorizeRequests()
.antMatchers("/", "/home", "/about").permitAll()
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.antMatchers("/user/**").access("hasRole('ROLE_USER')")
.anyRequest().authenticated();
}
其中,CookieCsrfTokenRepository.withHttpOnlyFalse()
指定 CSRF Token 放置在 HttpOnly Cookie 中。可以在 Http Response Header 中看到以下信息:
HTTP/1.1 200 OK
Set-Cookie: XSRF-TOKEN=k8_6gux8JKyl7z4hmsEUU3hE5_FJXr5zf1ZcI7CntAg; Path=/
CsrfHeaderFilter
和 CustomCsrfFilter
是为了拦截请求中的 CSRF Token,判断它是否与服务器端持有的 Token 一致,并对请求进行过滤。
public class CsrfHeaderFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
response.setHeader("X-CSRF-TOKEN", csrf.getToken());
}
filterChain.doFilter(request, response);
}
}
public class CustomCsrfFilter extends OncePerRequestFilter {
private final CsrfTokenRepository csrfTokenRepository;
public CustomCsrfFilter(CsrfTokenRepository csrfTokenRepository) {
this.csrfTokenRepository = csrfTokenRepository;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
CsrfToken csrfToken = csrfTokenRepository.loadToken(request); // 加载 CSRF Token
if (csrfToken != null) {
String csrfHeader = request.getHeader("X-CSRF-TOKEN"); // 获取请求中的 CSRF Token
if (csrfHeader != null && !csrfToken.getToken().equals(csrfHeader)) {
throw new ServletException("Invalid CSRF Token");
}
}
filterChain.doFilter(request, response);
}
}
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:spring security中的csrf防御原理(跨域请求伪造) - Python技术站