One way to protect against timing attacks when querying an indexed database field, would be to hash the value server-side. It's similar to how an attacker can't login if they obtained hashes of a password because the server still applies the hash before checking the database.
The original code (from the question above) has a number of issues, as marstato's answer pointed out, so let's start with this code instead:
$token = $_COOKIE['token'];
$db->query("SELECT COUNT(*) FROM sessions WHERE token = ?", $token);
The attacker can indeed incrementally find a valid token. It is typically hard or impossible, but in theory, this works. The attacker would query "A" and the server would ask the database for "A". Then they try "B" and the server asks the database for "B". If "A" was faster, they now try "AA" and "AB". If that is slower, they might try "BA" and "BB", etc. There's a lot more to it, but to recap the general idea of a timing attack, that's how it works.
To fix the issue, you could do this:
$secret = 'qJHcsgkED0egeuljhsZr'; // randomly generated
$hashed_token = hash_hmac('sha256', $_COOKIE['token'], $secret);
$db->query("SELECT COUNT(*) FROM sessions WHERE hashed_token = ?", $hashed_token);
Let's try the attack again: the attacker tries "A" and the server queries the database for "a661024ef...". The attacker tries "B" and the server queries the database for "5407624cb...". The timings are equal (or at least random) because neither will exist and it will never lead the attacker to a valid token.
This is, of course, dependent on the $secret
remaining secret. It would be best practice to change this regularly so that an old employee cannot know the secret and apply the attack, but this does mean that you have to also change all values in the database. If the database can do HMAC, you could work around that by doing something like this:
$secure_rnd = openssl_random_pseudo_bytes(16);
$randomized_token = hash_hmac('sha256', $_COOKIE['token'], $secure_rnd);
$db->query("SELECT COUNT(*) FROM sessions WHERE hmac(token, '$secure_rnd') = ?", $randomized_token);
Security-wise, this is the best approach for three reasons:
- While it is equivalent in speed to a non-indexed field, it prevents a future database admin from accidentally indexing the field. Typically, one would index a field when it is used for lookups. This leads to a timing attack, but how would you tell your future database administrator? Better have code in place such that it does not matter whether the field is indexed
- If the field were merely non-indexed, there may still be an attack: the database will go through each row and compare the values. Going through each row and comparing the first character is O(n), but if the database compares more characters (because more than zero characters match), it will take slightly longer, so an attacker could (again, theoretically) observe this slower response and learn that they guessed more than one character correct of any token. Using a
$secret
or $secure_rnd
is not vulnerable to that.
- It does not depend on a static
$secret
that may be known to old sysadmins.
The downside is that it is an O(n) slow query.
A hybrid approach, to take advantage of an index while avoiding a static $secret
, might be to use a $secret
that is changed daily. During the transition period every night, the application server could first try today's secret and, as a fallback, yesterday's secret. Since your tokens as well as the hashing method should be secure (otherwise you have bigger problems), looking for two values should never lead to collisions with other tokens. Once every database record is updated, it can stop querying for the old $secret
. The cron job that updates the database can write the secret(s) and upgrade status to a config file.
That said, I have never heard of anyone using a timing attack like this. Comparing passwords in a language like PHP, yes, but not with a database query. Most session token systems I know work in this theoretically-insecure way and yet they never seem to get hacked. That doesn't mean it's impossible, and especially if it seems 100% possible in theory, someone will probably make it work in practice sooner or later. While I'm not sure all this effort is necessary today for a small website, it would be good to protect high-security systems.