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 = "/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 添加到名为 _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("/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

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