Skip to main content

CSRF tokens and CSRF headers

CSRF Headers

Adding a custom request header to 'unsafe' outgoing AJAX requests (e.g. POST requests) adds some additional protection against CSRF attacks. Article here: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#custom-request-headers

The principle is that, due to browsers' Same-Origin policies, the header will only be sent to the server if the website it came from was within the same domain. This protection layer is fairly easy to implement.

Here, as an example, is how Deserted Chateau handles adding a custom request header to AJAX requests:

var csrf_token = $('meta[name="deserted-chateau-csrf-token"]').attr('content');

function csrfSafeMethod(method) {
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS)$/.test(method));
}

// Set request headers and CSRF tokens where needed
$.ajaxSetup({
    beforeSend: function (xhr, settings) {
        if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("deserted-chateau-anti-csrf-token", csrf_token);
        }
    },
    headers: {'deserted-chateau-anti-csrf-header': 'With your face, like a swindled partridge'},
});

You need to ensure this code is part of a script that loads on every single page on your website, or else it might not present on a page, causing a weakness in your security layer. The value and name of the header don't matter, other than that you should avoid picking a name that might conflict with other HTTP headers. The value doesn't need to be a secret, as this 'security layer' is relying on the browser policy sending this header, rather than any secrecy of the header value.

In case you were wondering why the CSRF header here has the value it does...

On the server, you then need to check that this HTTP header is present in any request where an 'unsafe' request was made (e.g. POST requests). An example function in PHP would look like this:

public static function verifyCSRFHeader() {
	$csrfHeaderValue = $_SERVER['HTTP_DESERTED_CHATEAU_ANTI_CSRF_HEADER'];
	$validHeader = ($csrfHeaderValue === "With your face, like a swindled partridge");
	if (!$validHeader) {
		// Log that an invalid header was received, if desired
	}
	// Return whether the header was present and valid; the calling function can then decide what to do
	return $validHeader;
}

Note the extra HTTP_ in the string given to the $_SERVER array, and that it has all dashes replaced with underscores.

CSRF Tokens

You might have noticed that in the previous example, as well as setting an HTTP request header with a fixed value, we also send a header which contains a "CSRF Token".

This token prevents CSRF attacks because the attacker cannot normally know the value of the token to send it with a malicious request, and so the request would be rejected by the server. Enabling CSRF tokens, other than the code above to include them in unsafe AJAX requests, involves adding a <meta> tag to each HTML page your users visit. For instance, here is a compact example in PHP to illustrate the concept:

<?php
session_start();
function generateAntiCSRFToken() {
    return bin2hex(random_bytes(64));
}

$antiCSRFToken = generateAntiCSRFToken();
$_SESSION['anti_csrf_token'] = $antiCSRFToken;
?>

<meta name="deserted-chateau-csrf-token" content="<?php echo $antiCSRFToken; ?>">

By including this PHP file at the start of every page, the <meta> tag with the CSRF token will be placed in every page to be used if needed. Note that the CSRF token should be created from cryptographically secure random values. Here, the PHP function random_bytes is used to generate a cryptographically secure random value, and then that value is converted to a hexadecimal string.

There is something else here, however. In this example, we are re-generating the CSRF token every single time a user browses to a new page, which is generally considered a rather "extreme" approach that you would only use in security-critical applications (e.g. banking), and can pose problems. There are two approaches here, but before looking at those, first we need to be sure we are verifying the CSRF token on the server properly.

Verifying CSRF token contents securely

Although the CSRF token values are just strings, you also need to pay attention to how you check they are valid. Normal string comparison operators like == and === have potential vulnerabilities against timing attacks, because they take more or less time to execute depending on how close to the start of the string the first non-matching character in the string was.

In PHP, the hash_equals() function can be used to securely compare strings for this purpose - note the documentation's warning that the user supplied string should be the second parameter given to this function.

Per-request CSRF tokens

The example above is one of "per-request" tokens, i.e. that every new page load regenerates the CSRF token. This is the most secure, because it means an attacker getting hold of a token has almost no use; the moment a user loads a new page, that token is no longer valid.

However, this comes with serious usability problems. Suppose the user has two tabs open on your website. They browse to a new page in one tab, which refreshes the CSRF token stored for that user's session on the server, and then the user tries to submit a form (like submitting a comment) in the other tab. The server will respond with an error, saying the CSRF token was not valid, because the CSRF token for that user's session was changed when they browsed to a different page of the website in the other tab.

Per-session CSRF tokens

With per-session CSRF tokens, the token is only refreshed when the user logs in, and the same token is then used for the user for that login session. This means the user won't have the same functionality problems when browsing in multiple tabs.

That being said, per-session CSRF tokens should still be refreshed periodically. An easy way to do this is to attach an expiry time for the CSRF token to the user's session, and whenever they browse to a new page, check if the expiry time is past the current time and regenerate the CSRF token if it is.