I had great feedback on my original stateless CSRF tokens post. Here is an improved version of stateless CSRF tokens based on that feedback:
[sourcecode lang=”php”]
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;
}
[/sourcecode]
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:
[sourcecode lang=”php”]
$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: $tokenn";
// confirm original works
echo "Should be valid: ";
if ( request_token_verify( $token, $data_str, $key ) ) {
echo "Valid tokenn";
} else {
echo "! INVALID ! tokenn";
}
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_tokenn";
echo "Should be INVALID: ";
if ( request_token_verify( $fake_token, $data_str, $key ) ) {
echo "Valid tokenn";
} else {
echo "! INVALID ! tokenn";
}
echo "n";
// alter timeout
list( $hash, $hash_time, $timeout, $random ) = explode( ‘-‘, $token, 4 );
$fake_token = "$hash-$hash_time-10000-$random";
echo "FAKE: $fake_tokenn";
echo "Should be INVALID: ";
if ( request_token_verify( $fake_token, $data_str, $key ) ) {
echo "Valid tokenn";
} else {
echo "! INVALID ! tokenn";
}
echo "n";
// new random
list( $hash, $hash_time, $timeout, $random ) = explode( ‘-‘, $token, 4 );
$fake_token = "$hash-$hash_time-$timeout-123";
echo "FAKE: $fake_tokenn";
echo "Should be INVALID: ";
if ( request_token_verify( $fake_token, $data_str, $key ) ) {
echo "Valid tokenn";
} else {
echo "! INVALID ! tokenn";
}
echo "n";
[/sourcecode]
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.