CSRF token collisions with multiple tabs
Asked Answered
E

3

21

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?

Ebneter answered 18/11, 2013 at 20:29 Comment(0)
H
21

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.

Herder answered 18/11, 2013 at 20:35 Comment(3)
Do I generate the token when a users first navigates to the login page, or in the login form action page? Right now I pass a CSRF token even on the login page to the login action.Ebneter
That's a little harder to answer. A CSRF token on a login page has a small amount of value -- less than tokens on forms that logged in users see, but more than frequently changing the tokens. All that a CSRF token on a login form really gets you is a slight amount of protection against a kind of distributed brute-force attack where the attacker tricks unsuspecting user's browsers into attempting logins and using another side-channel attack to see if they worked. On the other hand, consider a legit user who opens the login page, waits 20 minutes (expiring the session), and then tries a login.Herder
...the former is probably only a concern if you're a very large, very high-value site; the latter is a more likely situation that results in user frustration. I'd generate the token on the login action page.Herder
B
2

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.

Belated answered 18/11, 2013 at 20:31 Comment(3)
Do I ever flush tokens out of the array? If a user opens 11 tabs, in theory there would be 11 elements in the CSRF tokens array?Ebneter
I disagree. Generating multiple tokens per session is not "better."Herder
@josh3736: fair enough. My answer wasn't really clear; I meant "better" if he really wants one per page. I have edited it to clarify.Belated
S
0

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.

Spiral answered 23/6, 2020 at 1:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.