A versão anterior da resposta aceita ( md5(uniqid(mt_rand(), true))
) é insegura e oferece apenas cerca de 2 ^ 60 resultados possíveis - bem dentro do intervalo de uma busca de força bruta em cerca de uma semana para um atacante de baixo orçamento:
Como uma chave DES de 56 bits pode sofrer força bruta em cerca de 24 horas , e um caso médio teria cerca de 59 bits de entropia, podemos calcular 2 ^ 59/2 ^ 56 = cerca de 8 dias. Dependendo de como essa verificação de token é implementada, pode ser possível vazar praticamente informações de tempo e inferir os primeiros N bytes de um token de redefinição válido .
Já que a pergunta é sobre "melhores práticas" e começa com ...
Quero gerar um identificador para esqueci a senha
... podemos inferir que este token tem requisitos de segurança implícitos. E quando você adiciona requisitos de segurança a um gerador de números aleatórios, a prática recomendada é sempre usar um gerador de números pseudo-aleatórios criptograficamente seguro (abreviado CSPRNG).
Usando um CSPRNG
No PHP 7, você pode usar bin2hex(random_bytes($n))
(onde $n
é um número inteiro maior que 15).
No PHP 5, você pode usar random_compat
para expor a mesma API.
Alternativamente, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
se você ext/mcrypt
instalou. Outra boa linha é bin2hex(openssl_random_pseudo_bytes($n))
.
Separando a pesquisa do validador
Puxando do meu trabalho anterior sobre cookies "lembrar-me" seguros em PHP , a única maneira eficaz de mitigar o vazamento de tempo mencionado anteriormente (normalmente introduzido pela consulta de banco de dados) é separar a pesquisa da validação.
Se sua tabela for assim (MySQL) ...
CREATE TABLE account_recovery (
id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT
userid INTEGER(11) UNSIGNED NOT NULL,
token CHAR(64),
expires DATETIME,
PRIMARY KEY(id)
);
... você precisa adicionar mais uma coluna, selector
assim:
CREATE TABLE account_recovery (
id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT
userid INTEGER(11) UNSIGNED NOT NULL,
selector CHAR(16),
token CHAR(64),
expires DATETIME,
PRIMARY KEY(id),
KEY(selector)
);
Use um CSPRNG Quando um token de redefinição de senha é emitido, envie ambos os valores para o usuário, armazene o seletor e um hash SHA-256 do token aleatório no banco de dados. Use o seletor para obter o hash e a ID do usuário, calcule o hash SHA-256 do token que o usuário fornece com aquele armazenado no banco de dados usando hash_equals()
.
Código de exemplo
Gerando um token de redefinição no PHP 7 (ou 5.6 com random_compat) com PDO:
$selector = bin2hex(random_bytes(8));
$token = random_bytes(32);
$urlToEmail = 'http://example.com/reset.php?'.http_build_query([
'selector' => $selector,
'validator' => bin2hex($token)
]);
$expires = new DateTime('NOW');
$expires->add(new DateInterval('PT01H')); // 1 hour
$stmt = $pdo->prepare("INSERT INTO account_recovery (userid, selector, token, expires) VALUES (:userid, :selector, :token, :expires);");
$stmt->execute([
'userid' => $userId, // define this elsewhere!
'selector' => $selector,
'token' => hash('sha256', $token),
'expires' => $expires->format('Y-m-d\TH:i:s')
]);
Verificando o token de redefinição fornecido pelo usuário:
$stmt = $pdo->prepare("SELECT * FROM account_recovery WHERE selector = ? AND expires >= NOW()");
$stmt->execute([$selector]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($results)) {
$calc = hash('sha256', hex2bin($validator));
if (hash_equals($calc, $results[0]['token'])) {
// The reset token is valid. Authenticate the user.
}
// Remove the token from the DB regardless of success or failure.
}
Esses fragmentos de código não são soluções completas (evitei a validação de entrada e as integrações de estrutura), mas devem servir como um exemplo do que fazer.