Spring Security CSRF 保护指南

1. 概述

在本教程中,我们将讨论跨站点请求伪造(CSRF)攻击以及如何使用Spring Security防止它们。

延伸阅读:

使用Spring MVC和Thymeleaf进行CSRF保护

使用Spring Security,Spring MVC和Thymeleaf防止CSRF攻击的快速实用指南。

Spring 引导安全自动配置

Spring Boot 默认 Spring Security 配置的快速实用指南。

弹簧方法安全性简介

使用 Spring 安全性框架的方法级安全性指南。

2. 两种简单的CSRF攻击

CSRF 攻击有多种形式。让我们讨论一些最常见的。

2.1. GET示例

让我们考虑登录用户用于将资金转移到特定银行账户1234 的以下GET请求:

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">
Show Kittens Pictures
</a>
  • 图像 – 攻击者可能使用带有目标 URL 的 <img/>tag 作为图像源。换句话说,甚至不需要点击。当页面加载时,请求将自动执行:
<img src="http://bank.com/transfer?accountNo=5678&amount=1000"/>

2.2. POST示例

假设主请求需要是一个 POST 请求:

POST http://bank.com/transfer
accountNo=1234&amount=100

在这种情况下,攻击者需要让受害者运行类似的请求:

POST http://bank.com/transfer
accountNo=5678&amount=1000

在这种情况下,<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. 实际模拟

现在我们了解了CSRF攻击的样子,让我们在Spring应用程序中模拟这些示例。

我们将从一个简单的控制器实现开始 —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>

此页面将在另一个域(攻击者域)上运行。

最后,让我们在本地运行原始应用程序和攻击者应用程序。

要使攻击起作用,需要使用会话 cookie 对原始应用程序对用户进行身份验证。

让我们首先访问原始应用程序页面:

http://localhost:8081/spring-rest-full/csrfHome.html

它将在我们的浏览器上设置JSESSIONIDcookie。

然后让我们访问攻击者页面:

http://localhost:8081/spring-security-rest/api/csrfAttacker.html

如果我们跟踪来自此攻击者页面的请求,我们将能够发现命中原始应用程序的请求。由于JSESSIONIDcookie 会自动与这些请求一起提交,Spring 会像它们来自原始域一样对其进行身份验证。

3. 弹簧MVC应用

为了保护MVC应用程序,Spring为每个生成的视图添加一个CSRF令牌。此令牌必须在修改状态(PATCH、POST、PUT 和 DELETE - 而不是 GET)的每个 HTTP 请求上提交到服务器。这可以保护我们的应用程序免受 CSRF 攻击,因为攻击者无法从自己的页面获取此令牌。

接下来,我们将了解如何配置应用程序安全性以及如何使客户端符合它。

3.1. 弹簧安全配置

在较旧的XML配置(Spring Security 4之前),CSRF保护默认被禁用,我们可以根据需要启用它:

<http>
    ...
    <csrf />
</http>

从 Spring Security 4.x 开始,CSRF 保护默认处于启用状态。

此默认配置将 CSRF 令牌添加到名为 _csrf 的HttpServletRequest属性中。

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

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
      .csrf().disable();
    return http.build();
}

3.2. 客户端配置

现在我们需要在请求中包含 CSRF 令牌。

_csrf属性包含以下信息:

  • 令牌 – CSRF 令牌值
  • 参数名称 – HTML 表单参数的名称,必须包含令牌值
  • 标头名称 – HTTP 标头的名称,必须包含令牌值

如果我们的视图使用 HTML 表单,我们将使用参数 Name令牌值来添加隐藏的输入:

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

如果我们的视图使用 JSON,我们需要使用headerName令牌值来添加 HTTP 标头。

我们首先需要在元标记中包含令牌值和标头名称:

<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>

然后让我们使用 JQuery 检索元标记值:

var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");

最后,让我们使用这些值来设置 XHR 标头:

$(document).ajaxSend(function(e, xhr, options) {
    xhr.setRequestHeader(header, token);
});

4. 无状态弹簧 API

让我们回顾一下前端使用的无状态 Spring API 的情况。

正如我们在专门文章中所解释的,我们需要了解我们的无状态 API 是否需要 CSRF 保护。

如果我们的无状态 API 使用基于令牌的身份验证,例如 JWT,我们不需要 CSRF 保护,我们必须禁用它,如前所述。

但是,如果我们的无状态 API 使用会话 cookie 身份验证,我们需要启用 CSRF 保护,如下文所示。

4.1. 后端配置

我们的无状态 API 不能像我们的 MVC 配置那样添加 CSRF 令牌,因为它不会生成任何 HTML 视图。

在这种情况下,我们可以使用CookieCsrfTokenRepository在cookie中发送CSRF令牌:

@Configuration
public class SpringSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
          .csrf()
          .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
        return http.build();
    }
}

此配置将在前端设置XSRF-TOKENcookie。由于我们将仅HTTP标志设置为false,因此前端将能够使用JavaScript检索此cookie。

4.2. 前端配置

使用 JavaScript,我们需要从document.cookie列表中搜索XSRF-TOKENcookie 值。

由于此列表存储为字符串,因此我们可以使用以下正则表达式检索它:

const csrfToken = document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, '$1');

然后,我们必须将令牌发送到每个修改 API 状态的 REST 请求:POST、PUT、DELETE 和 PATCH。

Spring 希望在X-XSRF-TOKEN标头中接收它。

我们可以简单地使用 JavaScriptFetchAPI 进行设置:

fetch(url, {
  method: 'POST',
  body: /* data to send */,
  headers: { 'X-XSRF-TOKEN': csrfToken },
})

5. CSRF 禁用测试

完成所有这些操作后,让我们进行一些测试。

让我们首先尝试在 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的测试

现在让我们启用 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令牌,POST请求将失败,这当然意味着早期的攻击不再是一种选择。

此外,测试中的csrf() 方法创建一个RequestPostProcessor,该处理器会自动在请求中填充有效的 CSRF 令牌以进行测试。

7. 结论

在本文中,我们讨论了几种CSRF攻击以及如何使用Spring Security来防止它们。

与往常一样,本文中介绍的代码可在GitHub 上找到。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值