Search code examples
phpjquerycsrfcsrf-protection

CSRF protection failing on mobiles


within index.php, I have placed this at the top

<?php
session_start();

function generate_secure_token($length = 16)
{
    return bin2hex(openssl_random_pseudo_bytes($length));
}

$_SESSION['csrf_token'] = generate_secure_token();
$token = $_SESSION['csrf_token'];

?>

Within my form I then have a hidden field

<input type="hidden" name="csrf_token" id="csrf_token" value="<?php echo $token; ?>">

Within my Javascript I make an Ajax request

submitHandler: function(form) {
    $.ajax({
        type: "POST",
        url: "php/process.php",
        dataType: "json",
        data: {
            'csrf_token': $("#csrf_token").val()
        }
    }).done(function(response) {
        if (response === 'success') {
            window.location.replace("thanks.php");
        }
    }).fail(function(jqXHR, textStatus) {
        return false;
    });
}

And then finally within process.php I check the CSRF

<?php
session_start();

$errors = array();
$userData = array();

if (!isset($_POST['csrf_token']) ||
    empty($_POST['csrf_token']) ||
    $_POST['csrf_token'] != $_SESSION['csrf_token']) {
    $errors['csrf_token'] = 'Something went wrong';
}

if (!empty($errors)) {
    echo json_encode('failure');
    sendErrorEmail($errors, "Validation", $userData, __LINE__);
} else {
    //Do something
}

I have noticed that I am getting a lot of error emails relating to the CSRF token not being set. Within sendErrorEmail I am sending myself the browser information for those that fail, and I have noticed that 90% of them are IPhone or Android.

Is there anything specific to this code that may not work within smart phones?

Thanks


Solution

  • You are regenerating the CSRF token on every request to index.php, so if a user opens something up in a new window/tab after visiting the form page, their token won't validate when they try to submit the form, which would explain why the results you are seeing are also inconsistent. I suggest instead making the CSRF token last for the user's entire session:

    <?php
    session_start();
    
    function generate_secure_token($length = 16)
    {
        if (!isset($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(openssl_random_pseudo_bytes($length));
        }
    }
    
    generate_secure_token();
    

    The point of using CSRF tokens is to prevent a remote website from causing a currently logged in user from executing unwanted actions on your website. In order to execute the action, a token is required, and this token is only supplied by your web application. If the token is pseudo-randomly generated (as in your case), guessing it is already essentially impossible, so regenerating the token on each request does not add much to the overall security, unless your app has some sort of other vulnerability (e.g. XSS) that could then cause the token to be leaked back to the malicious website.

    See also: New CSRF token per request or NOT?