Course – LSS – NPI (cat=Spring Security)
announcement - icon

If you're working on a Spring Security (and especially an OAuth) implementation, definitely have a look at the Learn Spring Security course:

>> LEARN SPRING SECURITY

1. Overview

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

Further reading:

CSRF Protection with Spring MVC and Thymeleaf

Quick and practical guide to preventing CSRF attacks with Spring Security, Spring MVC and Thymeleaf.

Spring Boot Security Auto-Configuration

A quick and practical guide to Spring Boot's default Spring Security configuration.

Introduction to Spring Method Security

A guide to method-level security using the Spring Security framework.

2. Two Simple CSRF Attacks

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

2.1. GET Examples

Let’s consider the following GET request used by a logged-in user to transfer money to a specific bank account 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:

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

Suppose the main request needs to be a POST request:

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.

The attacker will need a <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:

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

2.3. Practical Simulation

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

We’re going to start with a simple controller implementation — the 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>
<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>.

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.

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.

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.

3. Spring MVC Application

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.

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

3.1. Spring Security Configuration

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

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

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

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

If we need to, we can disable this configuration:

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

3.2. Client Configuration

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

The _csrf attribute contains the following information:

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

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

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:

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:

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

4. Stateless Spring API

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

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

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.

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

4.1. Back-end Configuration

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

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

@Configuration
public class SecurityWithCsrfCookieConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> 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.

4.2. Front-end Configuration

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

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.

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

We can simply set it with the JavaScript Fetch API:

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

5. CSRF Disabled Test

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:

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

@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

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

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

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.

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

7. Conclusion

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

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

Course – LSS (cat=Security/Spring Security)

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security:

>> CHECK OUT THE COURSE
res – Security (video) (cat=Security/Spring Security)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.