A Guide to CSRF Protection in 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.

Read more

Spring Boot Security Auto-Configuration

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

Read more

Introduction to Spring Method Security

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

Read more

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 users to transfer money to specific bank account “1234”:

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

If the attacker wants to transfer money from a victims’ 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 – so the click isn’t even necessary. The request will be automatically executed when the page loads:

<img src="https://bank.com/transfer?accountNo=5678&amount=1000"/>

2.2. POST Example

If the main request needs to be a POST request – for example:

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

Then the attacker needs to have the victim run a similar:

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

Neither the <a> or the <img/> will work in this case. The attacker will need a <form> – as follows:

<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 – as follows:

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

2.3. Practical Simulation

Now that we understand how 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.

Note that we’ve simulated both a GET through a simple link as well as a POST through a simple <form>.

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

<html>
<body>
    <a href="http://localhost:8080/transfer?accountNo=5678&amount=1000">Show Kittens Pictures</a>

    <img src="https://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 the two applications – the original and the attacker application – locally, and let’s access the original page first:

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

Then, let’s access the attacker page:

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

Tracking the exact requests that originate from this attacker page, we’ll be able to immediately spot the problematic request, hitting the original application and fully authenticated.

[[config””]]
=== 3. Spring Security Configuration

[[config””]]In order to use the Spring Security CSRF protection, we’ll first need to make sure we use the proper HTTP methods for anything that modifies state (PATCH, POST, PUT, and DELETE – not GET).

3.1. Java Configuration

CSRF protection is enabled by default in the Java configuration. We can still disable it if we need to:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
      .csrf().disable();
}

3.2. XML Configuration

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

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

Starting from Spring Security 4.x – the CSRF protection is enabled by default in the XML configuration as well; we can of course still disable it if we need to:

<http>
    ...
    <csrf disabled="true"/>
</http>

3.3. Extra Form Parameters

Finally, with CSRF protection enabled on the server side, we’ll need to include the CSRF token in our requests on the client side as well:

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

3.4. Using JSON

We can’t submit the CSRF token as a parameter if we’re using JSON; instead we can submit the token within the header.

We’ll first need to include the token in our page – and for that we can use meta tags:

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

Then we’ll construct the header:

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

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

4. CSRF Disabled Test

With all of that in place, we’ll move to 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());
    }
}

As you might have noticed, 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)));
    }
}

Note that, when the user had the right security credentials, the request was successfully executed – no extra information was required.

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

5. CSRF Enabled Test

Now, let’s enable the 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());
    }
}

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

Finally, notice the csrf() method in the test; this creates a RequestPostProcessor that will automatically populate a valid CSRF token in the request for testing purposes.

6. Conclusion

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

The full implementation of this tutorial can be found in the GitHub project – this is a Maven-based project, so it should be easy to import and run as it is.

Leave a Reply

Your email address will not be published.