Learn more about API Security in Course 3 and 6 of "REST With Spring":
1. Overview
In this tutorial, we will discuss Cross-Site Request Forgery CSRF attacks and how to prevent them using Spring Security.
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="http://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="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 the two applications – the original and the attacker application – locally, and let’s access the original page first:
http://localhost:8081/spring-security-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.
3. Spring Security Configuration
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 an Eclipse based project, so it should be easy to import and run as it is.