Stateless Spring Security Part 1: Stateless CSRF protection
Today with a RESTful architecture becoming more and more standard it might be worthwhile to spend some time rethinking your current security approaches. Within this small series of blog posts we’ll explore a few relatively new ways of solving web related security issues in a Stateless way. This first entry is about protecting your website against Cross-Site Request Forgery (CSRF).
Recap: What is Cross-Site Request Forgery?
CSRF attacks are based on lingering authentication cookies. After being logged in or otherwise identified as a unique visitor on a site, that site is likely to leave a cookie within the browser. Without explicitly logging out or otherwise removing this cookie, it is likely to remain valid for some time.
Another site can abuse this by having the browser make (Cross-Site) requests to the site under attack. For example including some javascript to make a POST to “http://siteunderattack.com/changepassword?pw=hacked” tag will have the browser make that request, attaching any (authentication) cookies still active for that domain to the request!
Even though the Single-Origin Policy (SOP) does not allow the malicious site access to any part of the response. As probably clear from the example above, the harm is already be done if the requested URL triggers any side-effects (state changes) in the background.
Common approach
The commonly used solution would be to introduce the requirement of a so-called shared secret CSRF-token and make it known to the client as part of a previous response.
The client is then required to ping it back to the server for any requests with side-effects. This can be done either directly within a form as hidden field or as a custom HTTP header. Either way other sites cannot successfully produce requests with the correct CSRF-token included, because SOP prevents responses from the server from being read cross-site. The issue with this approach is that the server needs to remember the value of each CSRF-token for each user inside a session.
Stateless approaches
1. Switch to a full and properly designed JSON based REST API.
Single-Origin Policy only allows cross-site HEAD/GET and POSTs. POSTs may only be one of the following mime-types: application/x-www-form-urlencoded, multipart/form-data, or text/plain. Indeed no JSON! Now considering GETs should never ever trigger side-effects in any properly designed HTTP based API, this leaves it up to you to simply disallow any non-JSON POST/PUT/DELETEs and all is well. For a scenario with uploading files (multipart/form-data) explicit CSRF protection is still needed.
2. Check the HTTP Referer header.
The approach from above could be further refined by checking for the presence and content of a Referer header for scenarios that are still susceptible, such as multipart/form-data POSTs. This header is used by browsers to designate which exact page (url) triggered a request. This could easily be used to check against the expected domain for the site. Note that if opting for such a check you should never allow requests without the header present.
3. Client-side generated CSRF-tokens.
Have the clients generate and send the same unique secret value in both a Cookie and a custom HTTP header. Considering a website is only allowed to read/write a Cookie for its own domain, only the real site can send the same value in both headers. Using this approach all you server has to do is check if both values are equal, on a stateless per request basis!
Implementation
Focussing on the 3rd approach for explicit but Stateless CSRF-token based security, lets see how this looks like in code using Spring Boot and Spring Security.
Within Spring Boot you get some nice default security settings which you can fine tune using your own configuration adapter. In this case all that is needed is to disable the default csrf behavior and add own own StatelessCSRFFilter:
Customize csrf protection
@EnableWebSecurity @Order(1) public class StatelessCSRFSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable().addFilterBefore( new StatelessCSRFFilter(), CsrfFilter.class); } }
And here is the implementation of the StatelessCSRFFilter:
Custom CSRF filter
public class StatelessCSRFFilter extends OncePerRequestFilter { private static final String CSRF_TOKEN = "CSRF-TOKEN"; private static final String X_CSRF_TOKEN = "X-CSRF-TOKEN"; private final RequestMatcher requireCsrfProtectionMatcher = new DefaultRequiresCsrfMatcher(); private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl(); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (requireCsrfProtectionMatcher.matches(request)) { final String csrfTokenValue = request.getHeader(X_CSRF_TOKEN); final Cookie[] cookies = request.getCookies(); String csrfCookieValue = null; if (cookies != null) { for (Cookie cookie : cookies) { if (cookie.getName().equals(CSRF_TOKEN)) { csrfCookieValue = cookie.getValue(); } } } if (csrfTokenValue == null || !csrfTokenValue.equals(csrfCookieValue)) { accessDeniedHandler.handle(request, response, new AccessDeniedException( "Missing or non-matching CSRF-token")); return; } } filterChain.doFilter(request, response); } public static final class DefaultRequiresCsrfMatcher implements RequestMatcher { private final Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$"); @Override public boolean matches(HttpServletRequest request) { return !allowedMethods.matcher(request.getMethod()).matches(); } } }
As expected the Stateless version doesn’t do much more than a simple equals() on both header values.
Client-side implementation
Client-side implementation is trivial as well, especially when using AngularJS. AngularJS already comes with build-in CSRF-token support. If you tell it what cookie to read from, it will automatically put and send its value into a custom header of your choosing. (The browser taking care of sending the cookie header itself.)
You can override AngularJS’s default names (XSRF instead of CSRF) for these as followed:
Set proper token names
$http.defaults.xsrfHeaderName = 'X-CSRF-TOKEN'; $http.defaults.xsrfCookieName = 'CSRF-TOKEN';
Furthermore if you want to generate a new token value per request you could add a custom interceptor to the $httpProvider as followed:
Interceptor to generate cookie
app.config(['$httpProvider', function($httpProvider) { //fancy random token, losely after https://gist.github.com/jed/982883 function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e16]+1e16).replace(/[01]/g,b)}; $httpProvider.interceptors.push(function() { return { 'request': function(response) { // put a new random secret into our CSRF-TOKEN Cookie before each request document.cookie = 'CSRF-TOKEN=' + b(); return response; } }; }); }]);
You can find a complete working example to play with at github.
Make sure you have gradle 2.0 installed and simply run it using “gradle build” followed by a “gradle run”. If you want to play with it in your IDE like eclipse, go with “gradle eclipse” and just import and run it from within your IDE (no server needed).
Disclaimer
Sometimes the classical CSRF-tokens are wrongfully deemed a solution against replay or brute-force attacks. The stateless approaches listed here does not cover this type of attack. Personally I feel both type of attacks should be covered at another level, such as using https and rate-limiting. Which I both consider a must for any data-entry on a public web site!
Reference: | Stateless Spring Security Part 1: Stateless CSRF protection from our JCG partner Robbert van Waveren at the JDriven blog. |
Hi Arthur, Thanks for that beautiful article ! May I suggest some additional bits ? In point 1 you could start by explicitly saying that it is possible to do without CSRF protection. I got that at the end of the paragraph. In point 2 you could mention that the Referer header cannot be spoofed. Not sure if that is the case myself. But it is what I assume, reading you. In point 3 I wonder what are these two headers, you talk of a cookie and a header, and then talk about both of them headers. I suppose a… Read more »
This article wasn’t written by me, but by my JDriven collegue Robbert van Waveren. This is not the first time they’ve published someone else’s article under my name. I’ve mailed JavaCodeGeeks many times about this issue, but they keep making the same mistake again and again unfortunately :-(
It is quite possible to forge a syntactically-correct JSON request using text/plain encoding. So JSON web services are not automatically safe. You would need to disallow text/plain encoding.
And what if, in the future, HTML forms are allowed to use JSON encoding?
You should be using secure random number generator, otherwise this is not particularly safe.
what if a Man-in-the-middle change contents of the request and updates both the cookie and header with same value? The request is still valid and pass the filter.
Please correct me if I am wrong.