OK, permettez-moi de dire ceci sans ambages: si vous placez des données utilisateur, ou quoi que ce soit dérivé de données utilisateur dans un cookie à cet effet, vous faites quelque chose de mal.
Là. Je l'ai dit. Nous pouvons maintenant passer à la réponse réelle.
Quel est le problème avec le hachage des données utilisateur, demandez-vous? Eh bien, cela se résume à la surface d'exposition et à la sécurité à travers l'obscurité.
Imaginez une seconde que vous êtes un attaquant. Vous voyez un cookie cryptographique défini pour le souvenir de moi sur votre session. Il fait 32 caractères de large. Gee. C'est peut-être un MD5 ...
Imaginons également une seconde qu'ils connaissent l'algorithme que vous avez utilisé. Par exemple:
md5(salt+username+ip+salt)
Maintenant, tout ce qu'un attaquant doit faire est de forcer brutalement le "sel" (qui n'est pas vraiment un sel, mais plus à ce sujet plus tard), et il peut maintenant générer tous les faux jetons qu'il veut avec n'importe quel nom d'utilisateur pour son adresse IP! Mais forcer brutalement un sel est difficile, non? Absolument. Mais les GPU modernes sont extrêmement bons dans ce domaine. Et à moins que vous n'utilisiez suffisamment de hasard (faites-le assez grand), il va tomber rapidement, et avec lui les clés de votre château.
En bref, la seule chose qui vous protège est le sel, qui ne vous protège pas vraiment autant que vous le pensez.
Mais attendez!
Tout cela était supposé que l'attaquant connaît l'algorithme! Si c'est secret et déroutant, alors vous êtes en sécurité, non? FAUX . Cette ligne de pensée a un nom: la sécurité par l'obscurité , sur laquelle il ne faut JAMAIS se fier.
La meilleure façon
La meilleure façon est de ne jamais laisser les informations d'un utilisateur quitter le serveur, à l'exception de l'ID.
Lorsque l'utilisateur se connecte, générez un gros jeton aléatoire (128 à 256 bits). Ajoutez cela à une table de base de données qui mappe le jeton à l'ID utilisateur, puis envoyez-le au client dans le cookie.
Et si l'attaquant devine le jeton aléatoire d'un autre utilisateur?
Eh bien, faisons un peu de calcul ici. Nous générons un jeton aléatoire de 128 bits. Cela signifie qu'il y a:
possibilities = 2^128
possibilities = 3.4 * 10^38
Maintenant, pour montrer à quel point ce nombre est absurde, imaginons que chaque serveur sur Internet (disons 50 000 000 aujourd'hui) essaie de forcer ce nombre à un rythme de 1 000 000 000 par seconde chacun. En réalité, vos serveurs fondraient sous une telle charge, mais jouons cela.
guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000
Donc, 50 quadrillions de suppositions par seconde. C'est rapide! Droite?
time_to_guess = possibilities / guesses_per_second
time_to_guess = 3.4e38 / 50,000,000,000,000,000
time_to_guess = 6,800,000,000,000,000,000,000
Donc 6,8 sextillions de secondes ...
Essayons de ramener cela à des chiffres plus amicaux.
215,626,585,489,599 years
Ou encore mieux:
47917 times the age of the universe
Oui, c'est 47917 fois l'âge de l'univers ...
En gros, ça ne va pas être fêlé.
Pour résumer:
La meilleure approche que je recommande est de stocker le cookie en trois parties.
function onLogin($user) {
$token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit
storeTokenForUser($user, $token);
$cookie = $user . ':' . $token;
$mac = hash_hmac('sha256', $cookie, SECRET_KEY);
$cookie .= ':' . $mac;
setcookie('rememberme', $cookie);
}
Ensuite, pour valider:
function rememberMe() {
$cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : '';
if ($cookie) {
list ($user, $token, $mac) = explode(':', $cookie);
if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) {
return false;
}
$usertoken = fetchTokenByUserName($user);
if (hash_equals($usertoken, $token)) {
logUserIn($user);
}
}
}
Remarque: N'utilisez pas le jeton ou la combinaison utilisateur et jeton pour rechercher un enregistrement dans votre base de données. Veillez toujours à extraire un enregistrement en fonction de l'utilisateur et à utiliser une fonction de comparaison sans temporisation pour comparer le jeton récupéré par la suite. En savoir plus sur le chronométrage des attaques .
Maintenant, il est très important que ce SECRET_KEY
soit un secret cryptographique (généré par quelque chose comme /dev/urandom
et / ou dérivé d'une entrée à haute entropie). En outre, GenerateRandomToken()
doit être une source aléatoire forte ( mt_rand()
n'est pas assez forte. Utilisez une bibliothèque, telle que RandomLib ou random_compat , ou mcrypt_create_iv()
avec DEV_URANDOM
) ...
Il hash_equals()
s'agit d'empêcher les attaques de synchronisation . Si vous utilisez une version PHP inférieure à PHP 5.6, la fonction hash_equals()
n'est pas prise en charge. Dans ce cas, vous pouvez remplacer hash_equals()
par la fonction timingSafeCompare:
/**
* A timing safe equals comparison
*
* To prevent leaking length information, it is important
* that user input is always used as the second parameter.
*
* @param string $safe The internal (safe) value to be checked
* @param string $user The user submitted (unsafe) value
*
* @return boolean True if the two strings are identical.
*/
function timingSafeCompare($safe, $user) {
if (function_exists('hash_equals')) {
return hash_equals($safe, $user); // PHP 5.6
}
// Prevent issues if string length is 0
$safe .= chr(0);
$user .= chr(0);
// mbstring.func_overload can make strlen() return invalid numbers
// when operating on raw binary strings; force an 8bit charset here:
if (function_exists('mb_strlen')) {
$safeLen = mb_strlen($safe, '8bit');
$userLen = mb_strlen($user, '8bit');
} else {
$safeLen = strlen($safe);
$userLen = strlen($user);
}
// Set the result to the difference between the lengths
$result = $safeLen - $userLen;
// Note that we ALWAYS iterate over the user-supplied length
// This is to prevent leaking length information
for ($i = 0; $i < $userLen; $i++) {
// Using % here is a trick to prevent notices
// It's safe, since if the lengths are different
// $result is already non-0
$result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
}
// They are only identical strings if $result is exactly 0...
return $result === 0;
}