Background
When a user is using a web browser to log in to a web server, the browser remembers enough stuff so the user does not need to log in again when navigating to another page on the same server (by sending Cookies, the Authorization:-header etc for every request). This is very convenient for Bad Guys: they don’t have to hack the web server, hack the machine of the user, phish the user credentials or other difficult tasks - all they have to do is to execute a call in the web browser and hope the user was logged in to his/her bank (or whatever Bad Guys want to access). Doing this is surprisingly simple (send a HTML mail or link to a HTML page that in onLoad submits a form to the exploitable endpoint), and this is the reason for why we have CRSF – Cross-Site Request Forgery.
With CSRF eabled, the server will provide an extra token. This token will have to be kept by the client, and added to all requests that modifies state (POST, PUT, PATCH and DELETE). This is NOT done automatically by the web browser, and because this token is not available to the Bad Guys, their piggy-back attempt will fail. (Sidenote: As the response will not be available to the Bad Guy, requests that do not modify state (GET, HEAD and OPTIONS) are not that important to secure)
Spring with CSRF disabled (Solving the problem The WRONG Way!)
Spring has CSRF enabled by default, but as the inevitable first problem arises, internet is full of advice of just disabling it. Below is a Spring security config calling csrf.disable(), and what a POST request to a service "/services/reserveGUIDInc" looks like over the network (red text is client request, blue text is server response). Disabling CSRF is a simple one-liner, and instead of a pesky 401 the server now gives you requested data (in this example, "4") and a nice HTTP status code of 200 OK. Everything is good, right? Noooooo! Do not disable csrf! If you are authenticated to the server and I send you an evil email, makes you visit an evil web page, or in other ways makes your computer open a web connection to that service, I can piggy-back on your authenticated session and make my evil modification of server state (this is a slightly modified code from a real service, so here it would just imcrement a number. But still, someone uninvited should never be able to make a number increase!)
Keeping CSRF enabled, and still having a working system. (In other words - The Right Way)
Instead of showing a minimal and synthetic example, I will show the configuration of a real-world, in-use, slightly complex system. This is done deliberatly, partly so I as the text author can show some typical gotchas and partly so you as the reader will have to actually understand it and not walk away from here with only partly understanding the problem.
So, what does a real-life Spring configuration look like?
- We typically have the OPTION verb, some endpoints that everyone should be able to call (Swagger URLs, the "Login" and "Forgot Password" endpoints, some legacy endpoints that shouldn't be public but just has to be, etc), and the rest of the endpoints hidden behind an authentication wall. All of this is set up using authorizeRequest()
- There is usually some kind of user authentication (in this example a simple userDetailsService)
- Deciding if Cross-Origin Resource Sharing (CORS) is allowed or not
- What should happen when user logs in, logs out or if something fails spectacularly?
- and last but not least: if CSRF should be enabled (default) or disabled. CSRF by itself does not care if the endpoint is authenticated or not (there are perfectly valid reasons for having CRSF enabled on an unauthenticated endpoint, and although suspicious there might be reasons for having CSRF disabled on authenticated endpoints). However, CSRF needs some way of managing, transporting and verifying the csrf token. Depending on the architecture of the system, there might be multiple services involved, and thus also multiple tokens. In the configuration below, I use Spring CookieCsrfTokenRepository to manage the csrf token as a cookie. As I know there are two services involved in this particular system, I give it a custom and unique cookie name. I also use CsrfTokenResponseHeaderBindingFilter (from a very small and handy third-party library) to add the csrt token to the HTTP response object. This last part is what is often the cause of the whole Spring CSRF problem - Spring works with CSRF out-of-the-box if, and only if, you use Spring to render your pages on the server!
So, with this CSRF-enabled Spring security configuration, what does a HTTP GET look like? As above, red text is client request, blue text is server response. The call is an an authentication request, which is a common first encounter to a system. The client does not provide a CSRF token (the client probably doesn't even have a csrf token by now), but as a GET request by default is not CSRF enabled (GET does not modify state, right), the server happily sends a 200 OK HTTP response including all needed csrf information. I am not sure how often these change (the token is linked to a Spring session which might be reset), but if you for any reason handles these headers yourself, just make it a habit of updating your csrf repository whenever you see a X-CSRF_* header.
The meaning of these extra headers is quite simple:
- X-CSRF-HEADER tells you what header to add to your request if you send the csrf token as a header
- X-CSRF-PARAM tells you what URL parameter to add if you want to send the csrf token as a URL parameter
- X-CSRF-TOKEN tells you what csrf token value the server has assigned to your session. This is the token value Bad Guy does not have and which he is unable to add to his Evil request
Sending a POST request to a CSRF-enabled endpoint without prividing the expected csrf token is shown below. Note that even if a proper Authorization-header is sent (the very robust username=admin, password=admin), the response is 401 UNAUTHORIZED
Sending a POST request to a CSRF-enabled endpoint and providing the expected csrf token is shown below. The csrf token and header-name was kept from an earlier call. As you see, the resource is this time successfully created and a 201 CREATED is returned. Also note that the client (in this case, Postman) has cached and provides the CSRF-USERADMIN-TOKEN cookie. Having the csrf token as a cookie is one way of storing it client-side, but the client still have to copy this value over to either the header given in X-CSRF-HEADER or URL parameter given in X-CSRF-PARAM. This is the security of CSRF, the cookie in itself would have been reused by the browser and sent with an Evil request. When testing CSRF from a client with a cookie store, you might want to clear the cookie for each new session you initiates.
Tying the knots (how to get a client talk to CSRF-enabled systems)
We all agree talking to a CSRF-disabled server is easy. Too easy, actually (as it is even easy for Bad Guy). If client want to POST to a system (let us call it system A), it POSTs to System A - and hopefully get a 201 CREATED back. If part of the creation on System A involves creating something on System B, System A will simply POST to System B and check that it got 201 back, before finally sending the 201 response to client.
And finally: talking to a CSRF-enabled server. If client is just calling System A as if it was not using CSRF, the server will give a 401 UNAUTHORIZED response. Avoiding this roundtrip is easy, as the client knows if it has a valid csrf token or not. If the client does not have a valid csrf token, it will have to get one. Call authenticate endpoint, call a lightweight liveness/readiness endpoint, call whatever endpoint you want that is either doesn't change state (GET or HEAD) or a modifying state verb (POST, PUT, PATCH or DELETE) that is exempt from the csrf token requirement. The 200 OK response will include information about csrf header name, param name and token value. Store this and use it for the actual state-changing request. If part of the creation on System A involves creating something on System B, System A will have to do the same (send a GET request or similiar to get a csrf token for the session between System A and System B, and then send the actual request).
When client already has the csrf information, client can skip right to the actual request, making sure the csrf token is included in the request, and updating the token information if the response includes updated csrf information.
Actual (Java, using Unirest REST library) client code that first calls a cheap GET endpoint for getting a session csrf token and then appends this token to the actual POST call might look something like the code below. The same principles holds for any language and and way you construct your REST calls. And this is how you keep CSRF enabled in Spring and still have clients talk to the server, keeping Bad Guys away.
Also, note that quite a few libraries have native support for CSRF. In NodeJS clients I use axios for REST communication. Its request config by default uses the de facto cookie name 'XSRF-TOKEN' and header name 'X-XSRF-TOKEN' (and these can be changed). If the defaults matches your system, you won't have to do anything at all to have CSRF work in your NodeJS client!