A Guide to CSRF Protection in Spring Security – Spring Security中的CSRF保护指南

最后修改: 2016年 1月 25日

1. Overview

1.概述

In this tutorial, we will discuss Cross-Site Request Forgery (CSRF) attacks and how to prevent them using Spring Security.

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

2. Two Simple CSRF Attacks

2.两种简单的CSRF攻击

There are multiple forms of CSRF attacks. Let’s discuss some of the most common ones.

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

2.1. GET Examples

2.1.GET实例

Let’s consider the following GET request used by a logged-in user to transfer money to a specific bank account 1234:

让我们考虑下面这个GET请求,它是由一个登录的用户用来向一个特定的银行账户1234转账的。

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

If the attacker wants to transfer money from a victim’s account to his own account instead — 5678 — he needs to make the victim trigger the request:

如果攻击者想把钱从受害者的账户转到自己的账户,而不是–5678–他需要让受害者触发这个请求。

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

There are multiple ways to make that happen:

有多种方法可以实现这一目标。

  • Link – The attacker can convince the victim to click on this link, for example, to execute the transfer:
<a href="http://bank.com/transfer?accountNo=5678&amount=1000">
Show Kittens Pictures
</a>
  • Image – The attacker may use an <img/> tag with the target URL as the image source. In other words, the click isn’t even necessary. The request will be automatically executed when the page loads:
<img src="http://bank.com/transfer?accountNo=5678&amount=1000"/>

2.2. POST Example

2.2.POST实例

Suppose the main request needs to be a POST request:

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

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

In this case, the attacker needs to have the victim run a similar request:

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

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

Neither the <a> nor the <img/> tags will work in this case.

在这种情况下,<a><img/>标签都不会起作用。

The attacker will need a <form>:

攻击者将需要一个<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>

However, the form can be submitted automatically using JavaScript:

然而,该表格可以使用JavaScript自动提交。

<body onload="document.forms[0].submit()">
<form>
...

2.3. Practical Simulation

2.3.实际模拟

Now that we understand what a CSRF attack looks like, let’s simulate these examples within a Spring app.

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

We’re going to start with a simple controller implementation — the BankController:

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

And let’s also have a basic HTML page that triggers the bank transfer operation:

让我们也有一个基本的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>

This is the page of the main application, running on the origin domain.

这是主应用程序的页面,在原域上运行。

We should note that we’ve implemented a GET through a simple link and a POST through a simple <form>.

我们应该注意到,我们通过一个简单的链接实现了GET,通过一个简单的<form>实现了POST

Now let’s see what the attacker page would look like:

现在让我们看看攻击者的页面会是什么样子。

<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>

This page will run on a different domain — the attacker domain.

这个页面将在一个不同的域–攻击者域上运行。

Finally, let’s run both the original application and the attacker application locally.

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

To make the attack work, the user needs to be authenticated to the original application with a session cookie.

为了使攻击奏效,用户需要通过会话cookie对原始应用程序进行认证。

Let’s first access the original application page:

让我们首先访问原始申请页面。

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

It will set the JSESSIONID cookie on our browser.

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

Then let’s access the attacker page:

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

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

If we track the requests that originated from this attacker page, we’ll be able to spot the ones that hit the original application. As the JSESSIONID cookie is automatically submitted with these requests, Spring authenticates them as if they were coming from the original domain.

如果我们跟踪来自这个攻击者页面的请求,我们就能发现击中原始应用程序的请求。由于JSESSIONIDcookie会随着这些请求自动提交,Spring会对它们进行认证,就像它们来自原始域一样。

3. Spring MVC Application

3.Spring MVC应用

To protect MVC applications, Spring adds a CSRF token to each generated view. This token must be submitted to the server on every HTTP request that modifies state (PATCH, POST, PUT and DELETE — not GET). This protects our application against CSRF attacks since an attacker can’t get this token from their own page.

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

Next, we’ll see how to configure our application security and how to make our client compliant with it.

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

3.1. Spring Security Configuration

3.1.Spring安全配置

In the older XML config (pre-Spring Security 4), CSRF protection was disabled by default, and we could enable it as needed:

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

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

Starting from Spring Security 4.x, the CSRF protection is enabled by default.

从Spring Security 4.x开始,CSRF保护被默认启用。

This default configuration adds the CSRF token to the HttpServletRequest attribute named _csrf.

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

If we need to, we can disable this configuration:

如果我们需要,我们可以禁用这个配置。

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

3.2. Client Configuration

3.2.客户端配置

Now we need to include the CSRF token in our requests.

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

The _csrf attribute contains the following information:

_csrf属性包含以下信息。

  • token – the CSRF token value
  • parameterName – name of the HTML form parameter, which must include the token value
  • headerName – name of the HTTP header, which must include the token value

If our views use HTML forms, we’ll use the parameterName and token values to add a hidden input:

如果我们的视图使用HTML表单,我们将使用parameterNametoken值来添加一个隐藏输入。

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

If our views use JSON, we need to use the headerName and token values to add an HTTP header.

如果我们的视图使用JSON,我们需要使用headerNametoken值来添加一个HTTP头。

We’ll first need to include the token value and the header name in meta tags:

我们首先需要在元标签中包含令牌值和标题名称。

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

Then let’s retrieve the meta tag values with JQuery:

然后让我们用JQuery检索元标签的值。

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

Finally, let’s use these values to set our XHR header:

最后,让我们用这些值来设置我们的XHR头。

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

4. Stateless Spring API

4.无状态的Spring API

Let’s review the case of a stateless Spring API consumed by a front end.

让我们回顾一下由前端消费的无状态Spring API的情况。

As explained in our dedicated article, we need to understand if CSRF protection is required for our stateless API.

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

If our stateless API uses token-based authentication, such as JWT, we don’t need CSRF protection, and we must disable it as we saw earlier.

如果我们的无状态API使用基于令牌的认证,例如JWT,我们就不需要CSRF保护,而且我们必须像我们之前看到的那样禁用它。

However, if our stateless API uses a session cookie authentication, we need to enable CSRF protection as we’ll see next.

但是,如果我们的无状态API使用会话cookie认证,我们需要启用CSRF保护 正如我们接下来要看到的。

4.1. Back-end Configuration

4.1.后端配置

Our stateless API can’t add the CSRF token like our MVC configuration because it doesn’t generate any HTML view.

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

In that case, we can send the CSRF token in a cookie using CookieCsrfTokenRepository:

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

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

This configuration will set a XSRF-TOKEN cookie to the front end. Because we set the HTTP-only flag to false, the front end will be able to retrieve this cookie using JavaScript.

这个配置将给前端设置一个XSRF-TOKEN cookie。因为我们把HTTP-only标志设置为false,前端将能够使用JavaScript检索这个cookie。

4.2. Front-end Configuration

4.2.前端配置

With JavaScript, we need to search the XSRF-TOKEN cookie value from the document.cookie list.

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

As this list is stored as a string, we can retrieve it using this regex:

由于这个列表是以字符串的形式存储的,我们可以用这个重合词来检索它。

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

Then we must send the token to every REST request that modifies the API state: POST, PUT, DELETE and PATCH.

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

Spring expects to receive it in the X-XSRF-TOKEN header.

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

We can simply set it with the JavaScript Fetch API:

我们可以简单地用JavaScript Fetch API来设置它。

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

5. CSRF Disabled Test

5.CSRF禁用测试

With all of that in place, let’s do some testing.

有了所有这些,让我们做一些测试。

Let’s first try to submit a simple POST request when CSRF is disabled:

让我们首先尝试在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()); 
    } 
}

Here we’re using a base class to hold the common testing helper logic — the CsrfAbstractIntegrationTest:

在这里,我们使用一个基类来保存常见的测试辅助逻辑–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)));
    }
}

We should note that the request was successfully executed when the user had the right security credentials — no extra information was required.

我们应该注意到,当用户拥有正确的安全凭证时,该请求被成功执行–不需要额外的信息。

That means that the attacker can simply use any of the previously discussed attack vectors to compromise the system.

这意味着,攻击者可以简单地使用之前讨论的任何一种攻击载体来破坏系统。

6. CSRF Enabled Test

6.CSRF启用的测试

Now let’s enable CSRF protection and see the difference:

现在让我们启用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());
    }
}

We can see how this test is using a different security configuration — one that has the CSRF protection enabled.

我们可以看到这个测试使用的是不同的安全配置–一个启用了CSRF保护的配置。

Now the POST request will simply fail if the CSRF token isn’t included, which of course means that the earlier attacks are no longer an option.

现在,如果不包括CSRF令牌,POST请求将直接失败,这当然意味着早期的攻击不再是一种选择。

Furthermore, the csrf() method in the test creates a RequestPostProcessor that automatically populates a valid CSRF token in the request for testing purposes.

此外,测试中的csrf()方法创建了一个RequestPostProcessor,为测试目的在请求中自动填充了一个有效的CSRF标记。

7. Conclusion

7.结论

In this article, we discussed a couple of CSRF attacks and how to prevent them using Spring Security.

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

As always, the code presented in this article is available over on GitHub.

一如既往,本文介绍的代码可在GitHub上获得over on GitHub