Why is it common to put CSRF prevention tokens in cookies?
A good reason, which you have sort of touched on, is that once the CSRF cookie has been received, it is then available for use throughout the application in client script for use in both regular forms and AJAX POSTs. This will make sense in a JavaScript heavy application such as one employed by AngularJS (using AngularJS doesn't require that the application will be a single page app, so it would be useful where state needs to flow between different page requests where the CSRF value cannot normally persist in the browser).
Consider the following scenarios and processes in a typical application for some pros and cons of each approach you describe. These are based on the Synchronizer Token Pattern.
Request Body Approach
- User successfully logs in.
- Server issues auth cookie.
- User clicks to navigate to a form.
- If not yet generated for this session, server generates CSRF token, stores it against the user session and outputs it to a hidden field.
- User submits form.
- Server checks hidden field matches session stored token.
Advantages:
- Simple to implement.
- Works with AJAX.
- Works with forms.
- Cookie can actually be HTTP Only.
Disadvantages:
- All forms must output the hidden field in HTML.
- Any AJAX POSTs must also include the value.
- The page must know in advance that it requires the CSRF token so it can include it in the page content so all pages must contain the token value somewhere, which could make it time consuming to implement for a large site.
Custom HTTP Header (downstream)
- User successfully logs in.
- Server issues auth cookie.
- User clicks to navigate to a form.
- Page loads in browser, then an AJAX request is made to retrieve the CSRF token.
- Server generates CSRF token (if not already generated for session), stores it against the user session and outputs it to a header.
- User submits form (token is sent via hidden field).
- Server checks hidden field matches session stored token.
Advantages:
- Works with AJAX.
- Cookie can be HTTP Only.
Disadvantages:
- Doesn't work without an AJAX request to get the header value.
- All forms must have the value added to its HTML dynamically.
- Any AJAX POSTs must also include the value.
- The page must make an AJAX request first to get the CSRF token, so it will mean an extra round trip each time.
- Might as well have simply output the token to the page which would save the extra request.
Custom HTTP Header (upstream)
- User successfully logs in.
- Server issues auth cookie.
- User clicks to navigate to a form.
- If not yet generated for this session, server generates CSRF token, stores it against the user session and outputs it in the page content somewhere.
- User submits form via AJAX (token is sent via header).
- Server checks custom header matches session stored token.
Advantages:
- Works with AJAX.
- Cookie can be HTTP Only.
Disadvantages:
- Doesn't work with forms.
- All AJAX POSTs must include the header.
Custom HTTP Header (upstream & downstream)
- User successfully logs in.
- Server issues auth cookie.
- User clicks to navigate to a form.
- Page loads in browser, then an AJAX request is made to retrieve the CSRF token.
- Server generates CSRF token (if not already generated for session), stores it against the user session and outputs it to a header.
- User submits form via AJAX (token is sent via header) .
- Server checks custom header matches session stored token.
Advantages:
- Works with AJAX.
- Cookie can be HTTP Only.
Disadvantages:
- Doesn't work with forms.
- All AJAX POSTs must also include the value.
- The page must make an AJAX request first to get the CRSF token, so it will mean an extra round trip each time.
Set-Cookie
- User successfully logs in.
- Server issues auth cookie.
- User clicks to navigate to a form.
- Server generates CSRF token, stores it against the user session and outputs it to a cookie.
- User submits form via AJAX or via HTML form.
- Server checks custom header (or hidden form field) matches session stored token.
- Cookie is available in browser for use in additional AJAX and form requests without additional requests to server to retrieve the CSRF token.
Advantages:
- Simple to implement.
- Works with AJAX.
- Works with forms.
- Doesn't necessarily require an AJAX request to get the cookie value. Any HTTP request can retrieve it and it can be appended to all forms/AJAX requests via JavaScript.
- Once the CSRF token has been retrieved, as it is stored in a cookie the value can be reused without additional requests.
Disadvantages:
- All forms must have the value added to its HTML dynamically.
- Any AJAX POSTs must also include the value.
- The cookie will be submitted for every request (i.e. all GETs for images, CSS, JS, etc, that are not involved in the CSRF process) increasing request size.
- Cookie cannot be HTTP Only.
So the cookie approach is fairly dynamic offering an easy way to retrieve the cookie value (any HTTP request) and to use it (JS can add the value to any form automatically and it can be employed in AJAX requests either as a header or as a form value). Once the CSRF token has been received for the session, there is no need to regenerate it as an attacker employing a CSRF exploit has no method of retrieving this token. If a malicious user tries to read the user's CSRF token in any of the above methods then this will be prevented by the Same Origin Policy. If a malicious user tries to retrieve the CSRF token server side (e.g. via curl
) then this token will not be associated to the same user account as the victim's auth session cookie will be missing from the request (it would be the attacker's - therefore it won't be associated server side with the victim's session).
As well as the Synchronizer Token Pattern there is also the Double Submit Cookie CSRF prevention method, which of course uses cookies to store a type of CSRF token. This is easier to implement as it does not require any server side state for the CSRF token. The CSRF token in fact could be the standard authentication cookie when using this method, and this value is submitted via cookies as usual with the request, but the value is also repeated in either a hidden field or header, of which an attacker cannot replicate as they cannot read the value in the first place. It would be recommended to choose another cookie however, other than the authentication cookie so that the authentication cookie can be secured by being marked HttpOnly. So this is another common reason why you'd find CSRF prevention using a cookie based method.
My best guess as to the answer: Consider these 3 options for how to get the CSRF token down from the server to the browser.
- In the request body (not an HTTP header).
- In a custom HTTP header, not Set-Cookie.
- As a cookie, in a Set-Cookie header.
I think the 1st one, request body (while demonstrated by the Express tutorial I linked in the question), is not as portable to a wide variety of situations; not everyone is generating every HTTP response dynamically; where you end up needing to put the token in the generated response might vary widely (in a hidden form input; in a fragment of JS code or a variable accessible by other JS code; maybe even in a URL though that seems generally a bad place to put CSRF tokens). So while workable with some customization, #1 is a hard place to do a one-size-fits-all approach.
The second one, custom header, is attractive but doesn't actually work, because while JS can get the headers for an XHR it invoked, it can't get the headers for the page it loaded from.
That leaves the third one, a cookie carried by a Set-Cookie header, as an approach that is easy to use in all situations (anyone's server will be able to set per-request cookie headers, and it doesn't matter what kind of data is in the request body). So despite its downsides, it was the easiest method for frameworks to implement widely.
Using a cookie to provide the CSRF token to the client does not allow a successful attack because the attacker cannot read the value of the cookie and therefore cannot put it where the server-side CSRF validation requires it to be.
The attacker will be able to cause a request to the server with both the auth token cookie and the CSRF cookie in the request headers. But the server is not looking for the CSRF token as a cookie in the request headers, it's looking in the payload of the request. And even if the attacker knows where to put the CSRF token in the payload, they would have to read its value to put it there. But the browser's cross-origin policy prevents reading any cookie value from the target website.
The same logic does not apply to the auth token cookie, because the server is expects it in the request headers and the attacker does not have to do anything special to put it there.