1、概览 {#1概览}
本文将带你了解什么是跨站请求伪造(CSRF)攻击?以及如何使用 Spring Security 来防范这些攻击。
2、两种简单的 CSRF 攻击行为 {#2两种简单的-csrf-攻击行为}
CSRF 攻击有多种形式。
2.1、GET 示例 {#21get-示例}
假如下面这个 GET 请求,用于一个已登录的用户向指定的银行账户 1234
转账:
GET http://bank.com/transfer?accountNo=1234&amount=100
如果攻击者想把钱从受害者的账户转到自己的账户(5678
),他需要让受害者触发请求:
GET http://bank.com/transfer?accountNo=5678&amount=1000
有多种方法可以实现这一点:
-
链接 - 攻击者可以说服/诱导受害者点击该链接,例如执行转账:
<a href="http://bank.com/transfer?accountNo=5678&amount=1000"> 点击展示美女图片 </a>
-
图片 - 攻击者可能会使用
<img/>
标签,将目标 URL 作为图片来源。换句话说,甚至不需要点击。请求将在页面加载时自动执行:<img src="http://bank.com/transfer?accountNo=5678&amount=1000"/>
所以,涉及到敏感的业务,千万不能用 GET 请求。
2.2、POST 示例 {#22post-示例}
假设转账 API 是一个 POST 请求。
POST http://bank.com/transfer
accountNo=1234&amount=100
在这种情况下,<a>
和 <img/>
标签都不起作用。
攻击者需要使用 <form>
:
<form action="http://bank.com/transfer" method="POST">
<input type="hidden" name="accountNo" value="5678"/>
<input type="hidden" name="amount" value="1000"/>
<input type="submit" value="Show Kittens Pictures"/>
</form>
然后,使用 JavaScript 自动提交表单:
<body onload="document.forms[0].submit()">
<form>
...
2.3、实战 {#23实战}
在 Spring 应用中模拟 CSRF 攻击。
创建一个 "银行" 应用,定义一个转账 API BankController
:
@Controller
public class BankController {
private Logger logger = LoggerFactory.getLogger(getClass());
@RequestMapping(value = "/transfer", method = RequestMethod.GET)
@ResponseBody
public String transfer(@RequestParam("accountNo") int accountNo,
@RequestParam("amount") final int amount) {
logger.info("Transfer to {}", accountNo);
...
}
@RequestMapping(value = "/transfer", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
public void transfer2(@RequestParam("accountNo") int accountNo,
@RequestParam("amount") final int amount) {
logger.info("Transfer to {}", accountNo);
...
}
}
还需要一个基本的 HTML 页面来触发银行转账操作:
<html>
<body>
<h1>CSRF test on Origin</h1>
<a href="transfer?accountNo=1234&amount=100">Transfer Money to John</a>
<form action="transfer" method="POST">
<label>Account Number</label>
<input name="accountNo" type="number"/>
<label>Amount</label>
<input name="amount" type="number"/>
<input type="submit">
</form>
</body>
</html>
这是在银行应用上运行的客户端页面。
如上,通过一个简单的链接实现了 GET
,通过一个简单的 <form>
实现了 POST
。
现在、来看看攻击者页面的样子:
<html>
<body>
<a href="http://localhost:8080/transfer?accountNo=5678&amount=1000">Show Kittens Pictures</a>
<img src="http://localhost:8080/transfer?accountNo=5678&amount=1000"/>
<form action="http://localhost:8080/transfer" method="POST">
<input name="accountNo" type="hidden" value="5678"/>
<input name="amount" type="hidden" value="1000"/>
<input type="submit" value="Show Kittens Picture">
</form>
</body>
</html>
该页面在不同的应用上运行,即攻击者的应用。
最后,在本地运行银行应用和攻击者应用。
要使攻击奏效,用户需要使用 Session cookie 对银行应用进行身份认证。
首先,访问银行应用页面:
http://localhost:8081/spring-rest-full/csrfHome.html
它将在浏览器上设置 JSESSIONID
cookie。
然后访问攻击者应用:
http://localhost:8081/spring-security-rest/api/csrfAttacker.html
追踪源自此页面的请求,能够发现那些针对银行应用的请求。由于 JSESSIONID
Cookie 会自动随这些请求一起提交,Spring 会将它们视为来自银行页面的请求进行身份认证。
3、Spring MVC 应用 {#3spring-mvc-应用}
为了保护 MVC 应用,Spring 会在每个生成的视图中添加一个 CSRF Token。该 Token 必须在每次修改状态的 HTTP 请求(PATCH、POST、PUT 和 DELETE)中提交给服务器。这可以保护应用免受 CSRF 攻击,因为攻击者无法从自己的页面获取此 Token。
3.1、Spring Security 配置 {#31spring-security-配置}
在旧版 XML 配置(Spring Security 4 之前)中,CSRF 保护默认是禁用的,可以根据需要启用它:
<http>
...
<csrf />
</http>
从 Spring Security 4.x 开始,默认启用 CSRF 保护。
该默认配置将 CSRF
Token 添加到名为 _csrf
的 HttpServletRequest
属性中。
如果需要,可以禁用此配置:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable();
return http.build();
}
3.2、客户端配置 {#32客户端配置}
现在,需要在请求中包含 CSRF Token。
_csrf
属性包含以下信息:
token
- CSRF Token 值parameterName
- HTML 表单参数的名称,其中必须包含 Token 值headerName
- HTTP Header 的名称,其中必须包含 Token 值
如果视图使用 HTML 表单,可以使用 parameterName
和 token
值添加隐藏 input:
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
如果视图使用 JSON,则需要使用 headerName
和 token
值添加 HTTP 请求头信息。
首先在 meta
标签中包含 Token 值和 Header 名称:
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
然后,用 JQuery 获取 meta
标签值:
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
最后,使用这些值来设置 XHR Header:
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
4、无状态 API {#4无状态-api}
无状态 API 是否需要 CSRF 保护?
如果无状态 API 使用基于 Token 的身份验证(如 JWT),就不需要 CSRF 保护。反之,如果使用 Session Cookie 进行身份验证,就需要启用 CSRF 保护
4.1、后端配置 {#41后端配置}
无状态 API 无法像 MVC 配置那样添加 CSRF
Token,因为它不会生成任何 HTML 视图。
在这种情况下,可以使用 CookieCsrfTokenRepository
在 Cookie 中发送 CSRF Token:
@Configuration
public class SecurityWithCsrfCookieConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
return http.build();
}
}
此配置将为前端设置一个名为 XSRF-TOKEN
的 Cookie。由于将 HTTP-only
标志设置为 false
,因此前端能使用 JavaScript 获取此 Cookie。
4.2、前端配置 {#42前端配置}
通过 JavaScript 从 document.cookie
列表中搜索 XSRF-TOKEN
Cookie 值。
由于该列表以字符串形式存储,因此可以使用此 regex (正则)进行检索:
const csrfToken = document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, '$1');
然后,必须向每个修改 API 状态的 REST
请求发送 Token: POST、PUT、DELETE 和 PATCH。
Spring 会通过 X-XSRF-TOKEN
Header 来接收它。
只需使用 JavaScript Fetch API 设置即可:
fetch(url, {
method: 'POST',
body: /* 发送给服务器的请求体 */,
headers: { 'X-XSRF-TOKEN': csrfToken },
})
5、CSRF 禁用测试 {#5csrf-禁用测试}
首先尝试在禁用 CSRF 时提交一个简单的 POST 请求:
@ContextConfiguration(classes = { SecurityWithoutCsrfConfig.class, ...})
public class CsrfDisabledIntegrationTest extends CsrfAbstractIntegrationTest {
@Test
public void givenNotAuth_whenAddFoo_thenUnauthorized() throws Exception {
mvc.perform(
post("/foos").contentType(MediaType.APPLICATION_JSON)
.content(createFoo())
).andExpect(status().isUnauthorized());
}
@Test
public void givenAuth_whenAddFoo_thenCreated() throws Exception {
mvc.perform(
post("/foos").contentType(MediaType.APPLICATION_JSON)
.content(createFoo())
.with(testUser())
).andExpect(status().isCreated());
}
}
如上,通过继承 CsrfAbstractIntegrationTest
类来获取常用的测试辅助方法。
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class CsrfAbstractIntegrationTest {
@Autowired
private WebApplicationContext context;
@Autowired
private Filter springSecurityFilterChain;
protected MockMvc mvc;
@Before
public void setup() {
mvc = MockMvcBuilders.webAppContextSetup(context)
.addFilters(springSecurityFilterChain)
.build();
}
protected RequestPostProcessor testUser() {
return user("user").password("userPass").roles("USER");
}
protected String createFoo() throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(new Foo(randomAlphabetic(6)));
}
}
注意,当用户拥有正确的凭证时,请求就会被成功执行,不需要额外的信息。
这意味着攻击者只需使用前面讨论过的任何攻击方式,就能入侵系统。
6、CSRF 启用测试 {#6csrf-启用测试}
现在启用 CSRF 保护,看看有什么不同:
@ContextConfiguration(classes = { SecurityWithCsrfConfig.class, ...})
public class CsrfEnabledIntegrationTest extends CsrfAbstractIntegrationTest {
@Test
public void givenNoCsrf_whenAddFoo_thenForbidden() throws Exception {
mvc.perform(
post("/foos").contentType(MediaType.APPLICATION_JSON)
.content(createFoo())
.with(testUser())
).andExpect(status().isForbidden());
}
@Test
public void givenCsrf_whenAddFoo_thenCreated() throws Exception {
mvc.perform(
post("/foos").contentType(MediaType.APPLICATION_JSON)
.content(createFoo())
.with(testUser()).with(csrf())
).andExpect(status().isCreated());
}
}
可以看到这次测试使用了不同的安全配置,即启用了 CSRF 保护。
现在,如果不包含 CSRF Token,POST 请求将直接失败,这当然意味着先前的攻击不再可行。
此外,测试中的 csrf()
方法会创建一个 RequestPostProcessor
,在请求中自动填充一个有效的 CSRF
Token,以便进行测试。
7、总结 {#7总结}
本文介绍了 CSRF 攻击的几种方式,以及如何在 Spring 应用中使用 Spring Security 来避免 CSRF 攻击。
Ref:https://www.baeldung.com/spring-security-csrf