1. Overview
In this tutorial, we will discuss Cross-Site Request Forgery (CSRF) attacks and how to prevent them using Spring Security.
Quick and practical guide to preventing CSRF attacks with Spring Security, Spring MVC and Thymeleaf.
A quick and practical guide to Spring Boot's default Spring Security configuration.
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().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()
.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.