Following my previous article regarding REST security, I have decided to further push my exploration of CSRF implementation in the case of web clients talking to REST services.
The example code resulting from those tests can be found on GitHub.
But first…
CSRF (Cross-Site Request Forgery) protection is important and should be mandatory for all applications with a minimum of concern about web security. The protection uses a clever trick (the Synchronizer Token Pattern) to ensure that your requests, the ones that modify stuff on the server-side, are not fakes emitted by a third party.
The Spring Security doc has a very good example of what a CSRF attack could do. Feel free to go and read it, I’ll wait…
…done? Great. Now you can see why implementing some sort of protection against those attacks is important, especially if your web application handles sensitive data.
Who are you… who, who… who, who
If your web client sends a request to the server, how can the server be sure that the request comes from the trusted client, and not from someone else?
Well one solution would be to send the identified client a random unique token, and require the client to send that same token back when sending a request to the server. This way if the server gets the same token back on the next request by the client, it knows that the client is the one he “conversed” with during the previous exchange. On the other hand, if it doesn’t get that token back then the received request is considered unsafe and must be blocked.
Most of the times this unique token is generated once per client session: while the session lasts, the exchanged CSRF token will be the same. But if needed one can generate a new token for each request (although this might create issues, as explained in this stack overflow answer).
Serving CSRF tokens
In practice, at the server side, we will let Spring Security generate the tokens for us. CSRF handling is “on” by default, so that’s taken care for us already.
In the example code, CSRF configuration happens (implicitly!) when we configure HttpSecurity as follows:
package codesandnotes.restsecurity; import com.allanditzel.springframework.security.web.csrf.CsrfTokenResponseHeaderBindingFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) public class ApplicationSecurity extends WebSecurityConfigurerAdapter { @Autowired private RESTAuthenticationEntryPoint authenticationEntryPoint; @Autowired private RESTAuthenticationFailureHandler authenticationFailureHandler; @Autowired private RESTAuthenticationSuccessHandler authenticationSuccessHandler; @Override protected void configure(AuthenticationManagerBuilder builder) throws Exception { builder.inMemoryAuthentication().withUser("user").password("user").roles("USER").and().withUser("admin") .password("admin").roles("ADMIN"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/rest/**").authenticated(); http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint); http.formLogin().successHandler(authenticationSuccessHandler); http.formLogin().failureHandler(authenticationFailureHandler); http.logout().logoutSuccessUrl("/"); // CSRF tokens handling http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class); } }
Should you want to disable CSRF’s handling by default, you can always use the following:
@Override protected void configure(HttpSecurity http) throws Exception { ... http.formLogin().failureHandler(authenticationFailureHandler); http.logout().logoutSuccessUrl("/"); http.csrf().disable(); // CSRF tokens handling http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class); }
But really, would you leave your application unsecured? … I thought so.
Making CSRF tokens accessible
One of the key parts in the code above, since we’re dealing with REST services, not servlets, is the CsrfTokenResponseHeaderBindingFilter class which is added as a filter.
This has been originally proposed to me by Allan Ditzel. What it does is it moves the CSRF data from the HttpServletRequest object where Spring Security has placed it, into the HttpServletResponse header that is sent back to the client. This makes the CSRF token easily accessible to the web client receiving it.
On a closer look, you will see that the class’ code is obvious and effective, and full credit goes to the man that kindly made it available to us on this GitHub repo. Check it out!
Handling CSRF tokens on the client side
This “REST Security” tutorial code on GitHub is an improvement on the code I used in my previous article about securing REST-based applications. As a reminder, I was using jQuery’s AJAX to converse with a REST-based server application, requesting client authentication before he could access a protected page.
I have extended that code so that it now fully handles CSRF tokens from the server. The token last received through a header response is initially stored in a cookie. When the user sends a request to the server, the token is sent with it.
Let’s start looking at the JavaScript code that runs when the user first arrives on the index.html page. The page requires the result of a GET request to “/rest/hello”, but the access to that information requires authentication…
jQuery(document).ready(function($) { $.ajax({ type: 'GET', url: '/rest/hello' }).done(function (data, textStatus, jqXHR) { var csrfToken = jqXHR.getResponseHeader('X-CSRF-TOKEN'); if (csrfToken) { var cookie = JSON.parse($.cookie('helloween')); cookie.csrf = csrfToken; $.cookie('helloween', JSON.stringify(cookie)); } $('#helloweenMessage').html(data.message); }).fail(function (jqXHR, textStatus, errorThrown) { if (jqXHR.status === 401) { // HTTP Status 401: Unauthorized var cookie = JSON.stringify({method: 'GET', url: '/', csrf: jqXHR.getResponseHeader('X-CSRF-TOKEN')}); $.cookie('helloween', cookie); window.location = '/login.html'; } else { console.error('Houston, we have a problem...'); } });
On the first run the server responds with a 401 (unauthorized), which is exactly what we expect. We therefore redirect the user to a login.html page, but not before we store in a cookie the content of the CSRF header param that was sent back to us.
On the login page, the user then submits his username and password:
$('#loginform').submit(function (event) { event.preventDefault(); var cookie = JSON.parse($.cookie('helloween')); var data = 'username=' + $('#username').val() + '&password=' + $('#password').val(); $.ajax({ data: data, headers: {'X-CSRF-TOKEN': cookie.csrf}, timeout: 1000, type: 'POST', url: '/login' }).done(function(data, textStatus, jqXHR) { window.location = cookie.url; }).fail(function(jqXHR, textStatus, errorThrown) { console.error('Booh! Wrong credentials, try again!'); }); });
The jQuery code submits the login details along with that CSRF token we stored before. Assuming the credentials are right, the user is redirected to the last visited URL which was also stored in that cookie. Back to that index.html page!
The index.html page now is authorized to get the data from “/rest/hello”. On success, the code makes sure to store the CSRF token sent back by the server in the response’s header, and accessible through the jqXHR object:
$.ajax({ type: 'GET', url: '/rest/hello' }).done(function (data, textStatus, jqXHR) { var csrfToken = jqXHR.getResponseHeader('X-CSRF-TOKEN'); if (csrfToken) { var cookie = JSON.parse($.cookie('helloween')); cookie.csrf = csrfToken; $.cookie('helloween', JSON.stringify(cookie)); } $('#helloweenMessage').html(data.message); }).fail(function (jqXHR, textStatus, errorThrown) { ... });
From there the user can click on the “POST something…” button, which sends a dummy POST request to the REST services. As always, the last CSRF token retrieved from the previous response’s headers is sent with the request:
$('#postButton').on('click', function () { event.preventDefault(); var cookie = JSON.parse($.cookie('helloween')); $.ajax({ data: {}, headers: {'X-CSRF-TOKEN': cookie.csrf}, timeout: 1000, type: 'POST', url: '/rest/hellopost' }).done(function(data, textStatus, jqXHR) { console.info("POST succeeded!!!"); }).fail(function(jqXHR, textStatus, errorThrown) { console.error('Problems when posting...'); }); });
By now you should be able to see the pattern:
- the server sends you a token,
- the client makes a request and returns the token to it,
- the server replies and sends back another token,
- the client makes its next request and returns that second token to the server,
- etc.
Per session CSRF tokens generation
Let’s go back to the server side for a moment. In Spring Security, the CSRF tokens are generated per session. When a session starts, a CSRF token is generated. If the session changes or times out, a new CSRF token will be returned by the server.
This can be observed by looking at the console logs output by the tutorial code:
>>>>> When GET /rest/hello fails jqXHR X-CSRF-TOKEN = 8c16d397-bbb1-4d26-a176-ca07f9ec5c0c >>>>> When GET /rest/hello fails JSESSIONID cookie = 1u5p1wygijp7pqz1ijcjpste >>>>> When GET /rest/hello fails no restsecurity cookie was found
The first GET request fails because we’re not authenticated. A session is already granted to the client, and a CSRF token is sent back in the response. We are then redirected to the login page.
Navigated to http://localhost:8080/login.html >>>>> At loginform submission JSESSIONID cookie = 1u5p1wygijp7pqz1ijcjpste >>>>> At loginform submission CSRF token in cookie = 8c16d397-bbb1-4d26-a176-ca07f9ec5c0c
We submit the login form using the CSRF stored in our cookie, which comes from the previously failed attempt’s response.
>>>>> When loginform is done jqXHR X-CSRF-TOKEN = 8c16d397-bbb1-4d26-a176-ca07f9ec5c0c >>>>> When loginform is done JSESSIONID cookie = n3vkg1rytxnnipe221dvf0sy >>>>> When loginform is done CSRF token in cookie = 8c16d397-bbb1-4d26-a176-ca07f9ec5c0c
Once authenticated (log inform is done) we notice that we have been given a new session!
We are then redirected to the main page where that GET request is sent again.
Navigated to http://localhost:8080/ >>>>> When index document is ready JSESSIONID cookie = n3vkg1rytxnnipe221dvf0sy >>>>> When index document is ready CSRF token in cookie = 8c16d397-bbb1-4d26-a176-ca07f9ec5c0c >>>>> When GET /rest/hello is done jqXHR X-CSRF-TOKEN = 185bd929-20fc-4257-a036-71436ae43e51 >>>>> When GET /rest/hello is done JSESSIONID cookie = n3vkg1rytxnnipe221dvf0sy >>>>> When GET /rest/hello is done CSRF token in cookie = 8c16d397-bbb1-4d26-a176-ca07f9ec5c0c
The GET request succeeds, but a new CSRF token is given to us! This is what we must use for our next request:
>>>>> When postButton is clicked JSESSIONID cookie = n3vkg1rytxnnipe221dvf0sy >>>>> When postButton is clicked CSRF token in cookie = 185bd929-20fc-4257-a036-71436ae43e51
Had we used the “old” CSRF token, we would have received back a 403 (Forbidden).
Renewing your CSRFs more frequently?
Nothing prevents us from changing the rules, though. I have not tried this myself (yet!), but we could imagine forcing the generation of a new CSRF token when, for example, the user accesses a particularly sensitive section of the web application. Such as right before submitting a bank transfer? Or maybe we need the stringiest security possible for our site and we want to generate per request tokens after all!
In any case, the frequency at which we might get a new CSRF token might change.
Also, let’s not forget that a session can time out. This implies a new login process at the client side, and a new CSRF token sent by the server. We might want to make sure we don’t miss that update!
In practice we should improve the tutorial’s code to watch for new X-CSRF-TOKEN values in the response headers, after every AJAX request, and update the stored CSRF token if needed: we should assume we never know when the server decides to change the token!
Never safe enough
This post should have given you enough material to allow you to protect your REST-based application against CSRF attacks.
Please do keep in mind that security is, more than anything, an evolving playground: what is safe today might not be safe enough tomorrow. In fact I am myself constantly learning new things about security and always trying to correct my own mistakes. As such I seriously welcome any improvement that you believe could strengthen the security of what is proposed here!
Until then: take care and be safe 😉
Cheers!
13 comments
Bhumi
10/02/2015 at 12:41Awesome article.Thanks for the valuable information !
IT Press Review – march 2015 | Jamkey
13/04/2015 at 22:45[…] https://www.codesandnotes.be/2015/02/05/spring-securitys-csrf-protection-for-rest-services-the-client… […]
Ivo
30/04/2015 at 19:46I appreciate your articles and I am learning a lot from them, but one thing is bothering me with your solution: you store the secret CSRF token in a cookie that’s associated with the same domain as the JSESSION cookie, localhost
So I believe your solution does NOT resolve the CSRF issue. When I create a malicious website with a malicious form to the same domain (localhost), BOTH the JSESSIONID AND helloween cookies are automatically sent with the request.
IMO the solution should be to either send the secret CSRF-token as a request parameter or as a seperate request header.
Ivo
30/04/2015 at 20:39Let me answer myself:
Your technique (to store the CSRF-token as a client-set cookie) is called “Cookie-to-Header Token”, see https://en.wikipedia.org/wiki/Cross-site_request_forgery
Although both cookies (JSESSIONID and helloween) ARE sent from the malicious site, it can’t read the cookie due to “same origin policy”, see https://en.wikipedia.org/wiki/Same-origin_policy and the server doesn’t use the helloween cookie.
codesandnotes
01/05/2015 at 10:28Hello Ivo,
Your remarks AND your answers are totally correct IMHO!
I faced the same concerns while working on a full-fledged AngularJS web client querying a Spring-based REST server application. Of course in this post’s code everything is running on the same local port, so one can forget about the fact that the “same origin policy” applies. Once you run the client and the server on separate ports though, you are forced to take CORS into account otherwise your browser does not want to hear about it.
While working on the AngularJS security, I came across the “security considerations” paragraph in the $http documentation, which says about receiving CSRF tokens that “Since only JavaScript that runs on your domain could read the cookie, your server can be assured that the XHR came from JavaScript running on your domain”. So we’re safe. In fact I even ended up sending the CSRF token to the client as a cookie (AngularJS $http has an automatic mechanism that takes the token from a cookie and includes it in the response’s headers).
Of course that shifts the problem to how good your CORS filtering on the server is: if your server validates everything from everybody, it could be an issue. Mozilla MDN has a great post about setting up the CORS filtering.
I hope I will soon be able to post an article discussing the AngularJS / Spring security solution I’ve come up with so far: client-side and server-side implementations. But for me to do that I will have to find back that elusive 25th hour of the day… 😉
Suman
13/05/2015 at 22:41Hi Codesandnotes,
Awesome article. Thanks a lot!
This code work perfectly fine when my angular html client accessing the RESTful APIs on the same origin/domain; but when I try to access the same APIs from a different origin, getting an error 403 – Access Forbidden – CSRF token error.
Requirement:
I have some restful APIs which needs to be CSRF protected. Also, these APIs will be accessed from different Origin/domain by Angular WEB UI.
Approach That I tried so far by extending your example:
1. Added a CORS filter – Reference https://spring.io/guides/gs/re…
response.setHeader(“Access-Control-Allow-Origin”, “t*”);
response.setHeader(“Access-Control-Allow-Methods”, “POST, GET, OPTIONS, DELETE”);
response.setHeader(“Access-Control-Max-Age”, “3600”);
response.setHeader(“Access-Control-Allow-Headers”, “x-requested-with”);
No Luck on the above.
Please suggest if I need to do anything else to make it work.
Thanks,
Suman
codesandnotes
15/05/2015 at 09:54Hello Suman,
Yes, as stated in the response to Ivo this setup will not work when web client and server are on different domains. I will soon (as soon as possible!) post a couple of articles with example code on how to handle this.
In the meantime, you can try specifying further the Allow-Origin header, as I seem to remember that some browsers do not like accepting all origins (that’s what the “*” indicates). So you can try specifying your web server. For example, one of my application allows “http://localhost:8080” as an origin.
You must then also specify
response.setHeader("Vary", "Origin");
because, as stated in MDN, “If the server specifies an origin host rather than “*”, then it must also include Origin in the Vary response header to indicate to clients that server responses will differ based on the value of the Origin request header”.My “Access-Control-Allow-Headers” value is
"Origin, X-Requested-With, Content-Type, Accept, " + CSRF.REQUEST_HEADER_NAME
(where CSRF.REQUEST_HEADER_NAME is the name of the header where I put my CSRF tokens… that’s needed if your client submits CSRF tokens through headers).Finally, I set “Access-Control-Allow-Credentials” to “true”.
That should help you for the CORS aspect on the server side, but there still is the matter of performing the query the right way on AngularJS. As a pointer, remember to check $http documentation (https://docs.angularjs.org/api/ng/service/$http): the principle is that:
1) you obtain the CSRF token to do your request (I use an “OPTIONS” request to get those back from the server).
2) you include the credentials (JSESSIONID and CSRF tokens) in the header when you do the POST.
It’s tricky, I known. I’ll try to have these articles ready as soon as I can.
Good work!
Handling CSRF tokens with Spring Security | Krzysztof Góralski
30/09/2015 at 19:07[…] Spring Security’s CSRF protection for REST services – client & server side by https://www.codesandnotes.be […]
Marc Collin
06/10/2015 at 19:25why on the get of “/rest/hello” you don’t add the cookie in the header like you do for the post?
headers: {‘X-CSRF-TOKEN’: cookie.csrf},
codesandnotes
07/10/2015 at 09:23Hi Marc,
You don’t need to apply CSRF on GETs, only on PATCH, POST, PUT and DELETE. This is Spring Security’s default configuration for CSRF.
Ko
07/10/2015 at 14:17With a secure and httponly JSESSION cookie this all is for nothing right?
codesandnotes
07/10/2015 at 14:57That’s what I asked myself at first. Then again, if OWASP recommends it there’s usually a good reason for it…
The best answer I found regarding your concern is here: http://stackoverflow.com/a/25475141
IMHO: JSESSIONIDs, CSRF protection, HTTPS or CORS alone are far from being silver bullets for a motivated hacker.
But put them together and your application will be up for a fight!
Cheers!
Lukyer
19/11/2015 at 18:50It is usually caused by Spring default CSRF protection.
If you use for example DELETE HTTP request from your JS code, it is required to send also CSRF protection headers.
It is not necessary to disable CSRF protection! Please, do not do that if not necessary.
**You can easily add CSRF AJAX/REST protection by:**
1.Adding meta headers to every page (use @layout.html or something):
2.Customizing your ajax requests to sent these headers for every request:
$(function () {
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);
});
});
Notice that i use thymeleaf, so i use th:content instead of content attribute.