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" 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 read 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 your 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 our own StatelessCSRFFilter:
@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:
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:
$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:
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!