ren

Easy CSRF protection for AJAX requests everywhere

Recommended Posts

Posted (edited)

There is already some information on how to do this but it's a bit scattered, so here's a quick tutorial...

This is how $session->CSRF->hasValidToken() handles AJAX requests, by getting the token name and value from request headers:

if($this->config->ajax && isset($_SERVER["HTTP_X_$tokenName"]) && $_SERVER["HTTP_X_$tokenName"] === $tokenValue) {
	$valid = true;
} else if($this->input->post($tokenName) === $tokenValue) {
	$valid = true; 
}

So all we need to do is to pass the name and value to our JavaScript and set the right headers with our AJAX request.

We could instead of setting headers put the name and value pair into the body of a POST request, as the second conditional doesn't care if the POST request is received via AJAX or not. But this limits the request to POST and the body data type to either multipart/form-data or application/x-www-form-urlencoded (because that's all that PHP's $_POST superglobal will handle). Admittedly this might be 99% of use cases, but for example if a GET request invokes significant processing then CSRF protection could be used to prevent a DOS attack on the endpoint. So, for the sake of a bit more flexibility, and to be more in keeping with what PW expects, we'll use request headers.

I find it neater to write the token name and value into HTML and retrieve that using JavaScript DOM functions, instead of writing JavaScript with PHP 😝. So somewhere, maybe in init.inc.php, get the token name and value:

$csrfTokenName = $session->CSRF->getTokenName();
$csrfTokenValue = $session->CSRF->getTokenValue();

Then in a global footer template:

<div class="js-csrf-token" style="display:none;" data-csrf-token-name="<?php echo $csrfTokenName; ?>" data-csrf-token-value="<?php echo $csrfTokenValue; ?>"></div>

Now to retrieve the token in JavaScript:

function getCsrfToken() {
	const csrfTokenNode = document.getElementsByClassName('js-csrf-token')[0];

	return {
		name: csrfTokenNode.getAttribute('data-csrf-token-name'),
		value: csrfTokenNode.getAttribute('data-csrf-token-value'),
	}
}

Then in any AJAX request:

const csrfToken = getCsrfToken();
let xhr = new XMLHttpRequest();

// ...
// open the request and do some configuration
// ...

xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); // tell the server and PW that it's an AJAX request
xhr.setRequestHeader('X-' + csrfToken.name, csrfToken.value); // create a header with the token

// ...
// send the request
// ...

And that's all, in the endpoint just do...

if ($session->CSRF->hasValidToken()) {
	// works for AJAX!
}

This has used the default CSRF token created for every session, but to create and use a different token just specify an id:

$csrfTokenName = $session->CSRF->getTokenName('myAjaxToken'); // this creates a token if one with the given id doesn't exist
$csrfTokenValue = $session->CSRF->getTokenValue('myAjaxToken');

and...

if ($session->CSRF->hasValidToken('myAjaxToken') {
// ...
}

 

Edited by ren
  • Like 9
  • Thanks 5

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.