0
votes

When a user comes to my login page, I generate a CSRF token:

if (empty($_SESSION['csrf_token']))
    $_SESSION['csrf_token'] = bin2hex(openssl_random_pseudo_bytes(32));

and then display it as a hidden input in my form:

<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">

When the user clicks the login button, an AJAX request submits the user's details. In this process, it checks to see if the CSRF form value matches with the session variable:

if (!empty($_POST['csrf_token'])) {

    if (hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {

        // Success! They matched

    } else {
        // CSRF session token did not match posted form token
        die();
    }

} else {
    // CSRF token was missing from posted form
    die();
}

In most cases, this works fine - the CSRF tokens match and they get logged in. But, in the event that a user goes to the login page and leaves it open for a couple of hours, and then comes back and attempts to submit the form without refreshing the page, then the PHP session will have expired and that causes the form to fail.

I'm assuming it fails because the $_SESSION['csrf_token'] variable is empty, because the session itself no longer exists. This is where the problem lies.

I have found a couple of solutions that could work but seem rather 'hacky'.

One solutions was to refresh the page when the user comes back to it after some inactivity using:

<META HTTP-EQUIV="REFRESH" CONTENT="csrf_timeout_in_seconds">

...and another solution was to use hashing from the sessionid and a server-side secret to create the CSRF token:

csrf = hash(sessionid+secret)

But, I'm thinking this would also suffer the same issue I'm currently having, because the session_id also exists in the session, meaning it also wouldn't exist when the session expires.

Would there be a problem using a PHP Cookie with a longer 'shelf-life' instead of the PHP session cookie which has a rather short life? Are there any alternative solutions that I'm missing?

2

2 Answers

0
votes

I would consider increasing the session.gc-maxlifetime in php.ini which will control when that session expires.

PHP docs: http://php.net/manual/en/session.configuration.php#ini.session.gc-maxlifetime

Also, while that doesn't solve the problem if a user is afk for longer than that setting, I'd have the login page refresh after X amount of time without any interaction. You could also consider having an endpoint where a CSRF token is generated for that session, then the login page would send an ajax request to get the token and you could call that service every X minutes which will update the session value.

0
votes

Answering my own question because I found an ideal solution

Ultimately, what I needed was a token that would last longer than the session, but still be secure.

After doing quite a bit of research, I found that storing the CSRF token as a browser cookie was quite normal practice - tons of websites do it, and it's just as secure.

I didn't want the cookie being permanently persistant because, in theory, if a hacker was trying to submit data on a user's behalf (a CSRF attack) eventually they would crack the right token.

To negate this, I created a cookie that only expires when the user's browser closes - this doesn't completely prevent the CSRF tokenn from being cracked but the less alive-time the CSRF token has, the less likely it is to be cracked.

Solution:

if (empty($_COOKIE['csrf_token'])) {

    $cookie_csrf_name = 'csrf_token';
    $cookie_csrf_value =  bin2hex(random_bytes(32));
    $cookie_csrf_expiry = 0;
    $cookie_csrf_secure_only = TRUE;
    $cookie_csrf_http_only = TRUE;
    setcookie($cookie_csrf_name, $cookie_csrf_value, $cookie_csrf_expiry, '/', 'example.com', $cookie_csrf_secure_only, $cookie_csrf_http_only);

}