19
votes

I built CSRF protection in my application, by simply generating a random token on every page load, putting it into session, and then binding the token to the <body> tag attribute like:

<body data-csrf-token="csrf_GeJf53caJD6Q5WzwAzfy">

Then on every form action or ajax request, I simply grab the token from the body tag and send it along.

This works great, except for a huge issue. Users are opening multiple tabs of the application, and I am seeing token collisions. For example, a user loads the first page and it generates a token, then they switch tabs, load another page, which generates a new token. Finally they switch back to the first page and submit a format action. This results in an invalid CSRF token error.

What is the best way to re-architect this to prevent collisions with multiple tabs, while keeping it as secure as possible.

Is simply generating a single token upon login the correct solution, instead of generating a new token on every page load?

3

3 Answers

20
votes

Assuming that your app is secured with SSL, then there is really no value created by generating new tokens on every page load. It doesn't stop an attacker who has exploited an XSS vulnerability – they'd have access to the freshly generated token anyway.

Remember what a CSRF token defends against: a malicious third-party page blindly trying to post data to your app in hopes that the user is logged in. In this kind of attack, the attacker would never have access to the CSRF token, so changing it frequently does no good.

Do not waste time and resources keeping track of multiple tokens per session. Just generate one at the start and be done.

2
votes

You could use a single token upon login. As @Josh3736 points out, this works just fine.

If you really want to have one token per page, you could store an array of valid tokens in $_SESSION. You would then expire individual tokens as they are used. You could also optionally expire them after some timeout period, but that is only meaningful if the timeout is shorter than your session timeouts. But, again, what are you really accomplishing with this? A single token is perfectly fine for CSRF purposes.

0
votes

I've run into this exact problem, on page load I was generating a CSRF token like this:

$_SESSION["token"] = bin2hex(random_bytes(32));

Multiple tabs was causing CSRF mismatches, so I changed to this:

if (!isset($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}

Server side I do this (watered down version):

$csrf = preg_replace("/[^a-zA-Z0-9]+/", "", $_POST["token"]);
if ($csrf !== $_SESSION["token"]) {
    // Give an error
    die ("No valid CSRF token provided");
}

This may protect against XSS attacks, but it wouldn't stop someone going to the page, getting the PHP session ID (from headers) and the CSRF token and using a tool like Postman or WGET to put together hack API posts, etc.

That may be why this question exists... understanding the scope of what the CSRF token is for protecting against.