OK, lasciatemi mettere senza mezzi termini: se stai inserendo dati utente o qualcosa derivato dai dati utente in un cookie per questo scopo, stai facendo qualcosa di sbagliato.
Là. L'ho detto. Ora possiamo passare alla risposta effettiva.
Cosa c'è che non va nell'hash dei dati utente, chiedi? Bene, si riduce alla superficie di esposizione e alla sicurezza attraverso l'oscurità.
Immagina per un secondo di essere un attaccante. Nella sessione viene visualizzato un cookie crittografico impostato per il ricordo di me. È largo 32 caratteri. Gee. Potrebbe essere un MD5 ...
Immaginiamo anche per un secondo che conoscono l'algoritmo che hai usato. Per esempio:
md5(salt+username+ip+salt)
Ora, tutto ciò che un utente malintenzionato deve fare è forzare bruscamente il "sale" (che non è proprio un sale, ma ne parleremo più avanti), e ora può generare tutti i token falsi che desidera con qualsiasi nome utente per il suo indirizzo IP! Ma forzare un sale bruto è difficile, giusto? Assolutamente. Ma le GPU dei giorni nostri sono estremamente brave in questo. E a meno che tu non usi abbastanza casualità (rendilo abbastanza grande), cadrà rapidamente e con esso le chiavi del tuo castello.
In breve, l'unica cosa che ti protegge è il sale, che non ti protegge davvero tanto quanto pensi.
Ma aspetta!
Tutto ciò prevedeva che l'attaccante conosce l'algoritmo! Se è segreto e confuso, allora sei al sicuro, giusto? SBAGLIATO . Quella linea di pensiero ha un nome: Sicurezza attraverso l'oscurità , che non dovrebbe MAI essere invocata.
Il modo migliore
Il modo migliore è quello di non lasciare mai le informazioni di un utente al server, tranne l'id.
Quando l'utente accede, genera un token casuale di grandi dimensioni (da 128 a 256 bit). Aggiungilo a una tabella del database che associa il token al userid, quindi invialo al client nel cookie.
E se l'attaccante indovina il token casuale di un altro utente?
Bene, facciamo un po 'di matematica qui. Stiamo generando un token casuale a 128 bit. Ciò significa che ci sono:
possibilities = 2^128
possibilities = 3.4 * 10^38
Ora, per mostrare quanto assurdamente grande sia quel numero, immaginiamo che tutti i server su Internet (diciamo 50.000.000 oggi) provino a forzare quel numero ad una velocità di 1.000.000.000 al secondo ciascuno. In realtà i tuoi server si scioglierebbero sotto tale carico, ma giochiamo.
guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000
Quindi 50 quadrilioni di ipotesi al secondo. È veloce! Destra?
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
Quindi 6,8 sextillion secondi ...
Proviamo a ridurlo a numeri più amichevoli.
215,626,585,489,599 years
O ancora meglio:
47917 times the age of the universe
Sì, 47917 volte l'età dell'universo ...
Fondamentalmente, non sarà rotto.
Quindi per riassumere:
L'approccio migliore che raccomando è di conservare il cookie in tre parti.
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);
}
Quindi, per convalidare:
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: non utilizzare il token o la combinazione di utente e token per cercare un record nel database. Assicurati sempre di recuperare un record in base all'utente e di utilizzare una funzione di confronto temporizzata per confrontare in seguito il token recuperato. Maggiori informazioni sugli attacchi temporali .
Ora, è molto importante che SECRET_KEY
sia un segreto crittografico (generato da qualcosa di simile /dev/urandom
e / o derivato da un input ad alta entropia). Inoltre, GenerateRandomToken()
deve essere una fonte casuale forte ( mt_rand()
non è abbastanza forte. Usa una libreria, come RandomLib o random_compat , o mcrypt_create_iv()
con DEV_URANDOM
) ...
Lo scopo hash_equals()
è prevenire attacchi temporali . Se usi una versione di PHP sotto PHP 5.6 la funzione hash_equals()
non è supportata. In questo caso è possibile sostituire hash_equals()
con la funzione 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;
}