OK, deixe-me colocar isso sem rodeios: se você estiver colocando dados do usuário ou algo derivado dos dados do usuário em um cookie para esse fim, está fazendo algo errado.
Lá. Eu disse isso. Agora podemos avançar para a resposta real.
O que há de errado com o hash de dados do usuário, você pergunta? Bem, tudo se resume à superfície de exposição e segurança através da obscuridade.
Imagine por um segundo que você é um atacante. Você vê um cookie criptográfico definido para o lembrete-me na sua sessão. Tem 32 caracteres de largura. Gee. Isso pode ser um MD5 ...
Vamos imaginar também por um segundo que eles conhecem o algoritmo que você usou. Por exemplo:
md5(salt+username+ip+salt)
Agora, tudo o que um invasor precisa fazer é forçar com força bruta o "sal" (que não é realmente um sal, mas mais sobre isso depois), e agora ele pode gerar todos os tokens falsos que quiser com qualquer nome de usuário para seu endereço IP! Mas forçar brutalmente um sal é difícil, certo? Absolutamente. Mas as GPUs modernas são extremamente boas nisso. E a menos que você use aleatoriedade suficiente nele (faça-o grande o suficiente), ele cairá rapidamente, e com ele as chaves do seu castelo.
Em resumo, a única coisa que protege você é o sal, que na verdade não o protege tanto quanto você pensa.
Mas espere!
Tudo isso foi previsto que o invasor conhece o algoritmo! Se é secreto e confuso, então você está seguro, certo? ERRADO . Essa linha de pensamento tem um nome: Segurança através da obscuridade , que NUNCA deve ser invocada.
A melhor maneira
A melhor maneira é nunca deixar as informações de um usuário deixarem o servidor, exceto a identificação.
Quando o usuário efetuar login, gere um token aleatório grande (de 128 a 256 bits). Adicione isso a uma tabela de banco de dados que mapeie o token para o ID do usuário e envie-o para o cliente no cookie.
E se o invasor adivinhar o token aleatório de outro usuário?
Bem, vamos fazer algumas contas aqui. Estamos gerando um token aleatório de 128 bits. Isso significa que existem:
possibilities = 2^128
possibilities = 3.4 * 10^38
Agora, para mostrar quão absurdamente grande esse número é, vamos imaginar todos os servidores da Internet (digamos 50.000.000 hoje) tentando forçar esse número a uma taxa de 1.000.000.000 por segundo cada. Na realidade, seus servidores derreteriam sob tal carga, mas vamos jogar isso.
guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000
Então, 50 quadrilhões de palpites por segundo. É rápido! Certo?
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
Então, 6,8 sextilhões de segundos ...
Vamos tentar reduzir isso para números mais amigáveis.
215,626,585,489,599 years
Ou melhor ainda:
47917 times the age of the universe
Sim, isso é 47917 vezes a idade do universo ...
Basicamente, não será quebrado.
Entao, para resumir:
A melhor abordagem que eu recomendo é armazenar o cookie com três 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);
}
Em seguida, 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: Não use o token ou a combinação de usuário e token para procurar um registro no seu banco de dados. Sempre certifique-se de buscar um registro com base no usuário e use uma função de comparação com tempo seguro para comparar o token buscado posteriormente. Mais sobre ataques de tempo .
Agora, é muito importante que SECRET_KEY
seja um segredo criptográfico (gerado por algo como /dev/urandom
e / ou derivado de uma entrada de alta entropia). Além disso, GenerateRandomToken()
precisa ser uma fonte aleatória forte ( mt_rand()
não é suficientemente forte. Use uma biblioteca, como RandomLib ou random_compat , ou mcrypt_create_iv()
with DEV_URANDOM
) ...
O hash_equals()
é para evitar ataques de tempo . Se você usa uma versão PHP abaixo do PHP 5.6, a função hash_equals()
não é suportada. Nesse caso, você pode substituir hash_equals()
pela função 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;
}