Bien, déjenme decir esto sin rodeos: si está colocando datos de usuario, o cualquier cosa derivada de los datos de usuario en una cookie para este propósito, está haciendo algo mal.
Ahí. Lo dije. Ahora podemos pasar a la respuesta real.
¿Qué tiene de malo el hashing de datos de usuario? Bueno, todo se reduce a la superficie de exposición y la seguridad a través de la oscuridad.
Imagina por un segundo que eres un atacante. Verá una cookie criptográfica establecida para recordarme en su sesión. Tiene 32 caracteres de ancho. Caramba. Eso puede ser un MD5 ...
Imaginemos por un segundo que conocen el algoritmo que usó. Por ejemplo:
md5(salt+username+ip+salt)
Ahora, todo lo que un atacante debe hacer es forzar la "sal" (que no es realmente una sal, pero más sobre eso más adelante), ¡y ahora puede generar todos los tokens falsos que quiera con cualquier nombre de usuario para su dirección IP! Pero forzar una sal con fuerza bruta es difícil, ¿verdad? Absolutamente. Pero las GPU modernas son extremadamente buenas en eso. Y a menos que uses suficiente aleatoriedad (hazlo lo suficientemente grande), caerá rápidamente y con él las llaves de tu castillo.
En resumen, lo único que te protege es la sal, que en realidad no te protege tanto como crees.
¡Pero espera!
¡Todo eso se basaba en que el atacante conoce el algoritmo! Si es secreto y confuso, entonces estás a salvo, ¿verdad? MAL . Esa línea de pensamiento tiene un nombre: Seguridad a través de la oscuridad , que NUNCA se debe confiar.
La mejor manera
La mejor manera es nunca dejar que la información de un usuario abandone el servidor, excepto la identificación.
Cuando el usuario inicie sesión, genere un token aleatorio grande (de 128 a 256 bits). Agregue eso a una tabla de base de datos que asigna el token al ID de usuario y luego envíelo al cliente en la cookie.
¿Qué pasa si el atacante adivina la ficha aleatoria de otro usuario?
Bueno, hagamos algunas matemáticas aquí. Estamos generando un token aleatorio de 128 bits. Eso significa que hay:
possibilities = 2^128
possibilities = 3.4 * 10^38
Ahora, para mostrar cuán absurdamente grande es ese número, imaginemos que cada servidor en Internet (digamos 50,000,000 hoy) intenta forzar ese número a una velocidad de 1,000,000,000 por segundo cada uno. En realidad, sus servidores se derretirían bajo tal carga, pero juguemos esto.
guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000
Entonces 50 cuatrillones de conjeturas por segundo. ¡Eso es rápido! ¿Correcto?
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
Entonces 6.8 sextillones de segundos ...
Intentemos reducir eso a números más amigables.
215,626,585,489,599 years
O mejor:
47917 times the age of the universe
Sí, eso es 47917 veces la edad del universo ...
Básicamente, no se va a romper.
Así que para resumir:
El mejor enfoque que recomiendo es almacenar la cookie con tres partes.
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);
}
Luego, para validar:
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);
}
}
}
Nota: No use el token o la combinación de usuario y token para buscar un registro en su base de datos. Siempre asegúrese de obtener un registro basado en el usuario y utilice una función de comparación segura para comparar el token obtenido posteriormente. Más acerca de los ataques de tiempo .
Ahora, es muy importante que SECRET_KEY
sea un secreto criptográfico (generado por algo como /dev/urandom
y / o derivado de una entrada de alta entropía). Además, GenerateRandomToken()
debe ser una fuente aleatoria fuerte ( mt_rand()
no es lo suficientemente fuerte. Utilice una biblioteca, como RandomLib o random_compat , o mcrypt_create_iv()
con DEV_URANDOM
) ...
El hash_equals()
es para evitar ataques de tiempo . Si usa una versión de PHP por debajo de PHP 5.6, la función hash_equals()
no es compatible. En este caso, puede reemplazarlo hash_equals()
con la función 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;
}