51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

使用 Spring Security 防止 CSRF 攻击

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 = &quot;/transfer&quot;, method = RequestMethod.GET)
@ResponseBody
public String transfer(@RequestParam(&quot;accountNo&quot;) int accountNo, 
  @RequestParam(&quot;amount&quot;) final int amount) {
    logger.info(&quot;Transfer to {}&quot;, accountNo);
    ...
}

@RequestMapping(value = &quot;/transfer&quot;, method = RequestMethod.POST) @ResponseStatus(HttpStatus.OK) public void transfer2(@RequestParam(&quot;accountNo&quot;) int accountNo, @RequestParam(&quot;amount&quot;) final int amount) { logger.info(&quot;Transfer to {}&quot;, accountNo); ... }

}

还需要一个基本的 HTML 页面来触发银行转账操作:

<html>
<body>
    <h1>CSRF test on Origin</h1>
    <a href="transfer?accountNo=1234&amount=100">Transfer Money to John</a>
&lt;form action=&quot;transfer&quot; method=&quot;POST&quot;&gt;
    &lt;label&gt;Account Number&lt;/label&gt; 
    &lt;input name=&quot;accountNo&quot; type=&quot;number&quot;/&gt;
&amp;lt;label&amp;gt;Amount&amp;lt;/label&amp;gt;         
&amp;lt;input name=&amp;quot;amount&amp;quot; type=&amp;quot;number&amp;quot;/&amp;gt;

&amp;lt;input type=&amp;quot;submit&amp;quot;&amp;gt;

&lt;/form&gt;

</body> </html>

这是在银行应用上运行的客户端页面。

如上,通过一个简单的链接实现了 GET,通过一个简单的 <form> 实现了 POST

现在、来看看攻击者页面的样子:

<html>
<body>
    <a href="http://localhost:8080/transfer?accountNo=5678&amount=1000">Show Kittens Pictures</a>
&lt;img src=&quot;http://localhost:8080/transfer?accountNo=5678&amp;amount=1000&quot;/&gt;

&lt;form action=&quot;http://localhost:8080/transfer&quot; method=&quot;POST&quot;&gt; &lt;input name=&quot;accountNo&quot; type=&quot;hidden&quot; value=&quot;5678&quot;/&gt; &lt;input name=&quot;amount&quot; type=&quot;hidden&quot; value=&quot;1000&quot;/&gt; &lt;input type=&quot;submit&quot; value=&quot;Show Kittens Picture&quot;&gt; &lt;/form&gt;

</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 添加到名为 _csrfHttpServletRequest 属性中。

如果需要,可以禁用此配置:

@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 表单,可以使用 parameterNametoken 值添加隐藏 input:

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

如果视图使用 JSON,则需要使用 headerNametoken 值添加 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(&quot;/foos&quot;).contentType(MediaType.APPLICATION_JSON)
        .content(createFoo())
      ).andExpect(status().isUnauthorized());
}

@Test public void givenAuth_whenAddFoo_thenCreated() throws Exception { mvc.perform( post(&quot;/foos&quot;).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(&quot;user&quot;).password(&quot;userPass&quot;).roles(&quot;USER&quot;); }

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(&quot;/foos&quot;).contentType(MediaType.APPLICATION_JSON)
        .content(createFoo())
        .with(testUser())
      ).andExpect(status().isForbidden());
}

@Test public void givenCsrf_whenAddFoo_thenCreated() throws Exception { mvc.perform( post(&quot;/foos&quot;).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

赞(2)
未经允许不得转载:工具盒子 » 使用 Spring Security 防止 CSRF 攻击