Better Stateless CSRF Tokens

I had great feedback on my original stateless CSRF tokens post. Here is an improved version of stateless CSRF tokens based on that feedback:

function request_token_generate( $data_str, $key, $timeout = 900 ) {
    $now = microtime( true );
    $range = mt_rand( 4, 25 );
#   $random = bin2hex( openssl_random_pseudo_bytes( $range ) );
    $random = bin2hex( fread( fopen( '/dev/urandom', 'r' ), $range ) );
    $hash = hash_hmac( 'sha256', "$data_str-$now-$timeout-$random", $key );

    return "$hash-$now-$timeout-$random";
}

function request_token_verify( $token, $data_str, $key ) {
    list( $hash, $hash_time, $timeout, $random ) = explode( '-', $token, 4 );
    if ( 
        empty( $hash )
        || empty( $hash_time )
        || empty( $timeout )
        || empty( $random )
    ) {
        return false;
    }

    if ( microtime( true ) > $hash_time + $timeout ) {
        return false;
    }

    $check_string = "$data_str-$hash_time-$timeout-$random";
    $check_hash = hash_hmac( 'sha256', $check_string, $key );

    if ( $check_hash === $hash ) {
        return true;
    }

    return false;
}

The biggest change is the addition of a random value, of various lengths, to the token. My first version of this used OpenSSL to generate the random values. In the end I opted to just open /dev/urandom directly. I left the openssl_random_pseudo_bytes() call in a comment to show how that works.

I updated the tests to include altering the random bits:

$key = '5up3R53cr3T!';
$data_str = '45873' . 'delete_post_345' . '2013-05-01 14:45:32' . '0dH6hi';
$token = request_token_generate( $data_str, $key, 15 );
echo "REAL TOKEN: $token\n";

// confirm original works
echo "Should be valid: ";
if ( request_token_verify( $token, $data_str, $key ) ) {
    echo "Valid token\n";
} else {
    echo "! INVALID ! token\n";
}
echo "\n";

// turn back time
list( $hash, $hash_time, $timeout, $random ) = explode( '-', $token, 4 );
$hash_time = $hash_time - 100;
$fake_token = "$hash-$hash_time-$timeout-$random";
echo "FAKE: $fake_token\n";

echo "Should be INVALID: ";
if ( request_token_verify( $fake_token, $data_str, $key ) ) {
    echo "Valid token\n";
} else {
    echo "! INVALID ! token\n";
}
echo "\n";

// alter timeout
list( $hash, $hash_time, $timeout, $random ) = explode( '-', $token, 4 );
$fake_token = "$hash-$hash_time-10000-$random";
echo "FAKE: $fake_token\n";

echo "Should be INVALID: ";
if ( request_token_verify( $fake_token, $data_str, $key ) ) {
    echo "Valid token\n";
} else {
    echo "! INVALID ! token\n";
}
echo "\n";

// new random
list( $hash, $hash_time, $timeout, $random ) = explode( '-', $token, 4 );
$fake_token = "$hash-$hash_time-$timeout-123";
echo "FAKE: $fake_token\n";

echo "Should be INVALID: ";
if ( request_token_verify( $fake_token, $data_str, $key ) ) {
    echo "Valid token\n";
} else {
    echo "! INVALID ! token\n";
}
echo "\n";

The random part detects tampering in the same way the timeout does, by including it in the original data string used by the HMAC.

While this is an improvement over my previous version it still isn’t the same as using a stateful CSRF token system. Make sure you are familiar with the trade offs of each before picking one.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>