La versión anterior de la respuesta aceptada ( md5(uniqid(mt_rand(), true))
) es insegura y solo ofrece aproximadamente 2 ^ 60 resultados posibles, dentro del rango de una búsqueda de fuerza bruta en aproximadamente una semana para un atacante de bajo presupuesto:
Dado que una clave DES de 56 bits puede ser forzada en aproximadamente 24 horas , y un caso promedio tendría aproximadamente 59 bits de entropía, podemos calcular 2 ^ 59/2 ^ 56 = aproximadamente 8 días. Dependiendo de cómo se implemente esta verificación de token, podría ser posible prácticamente filtrar información de tiempo e inferir los primeros N bytes de un token de reinicio válido .
Dado que la pregunta es sobre "mejores prácticas" y comienza con ...
Quiero generar un identificador para la contraseña olvidada
... podemos inferir que este token tiene requisitos de seguridad implícitos. Y cuando agrega requisitos de seguridad a un generador de números aleatorios, la mejor práctica es usar siempre un generador de números pseudoaleatorios criptográficamente seguro (abreviado CSPRNG).
Usando un CSPRNG
En PHP 7, puede usar bin2hex(random_bytes($n))
(donde $n
es un número entero mayor que 15).
En PHP 5, puede utilizar random_compat
para exponer la misma API.
Alternativamente, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
si ha ext/mcrypt
instalado. Otra buena frase es bin2hex(openssl_random_pseudo_bytes($n))
.
Separar la búsqueda del validador
Extrayendo de mi trabajo anterior sobre cookies seguras "recordarme" en PHP , la única forma efectiva de mitigar la filtración de tiempo antes mencionada (normalmente introducida por la consulta de la base de datos) es separar la búsqueda de la validación.
Si su tabla se ve así (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)
);
... necesitas agregar una columna más selector
, así:
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 un CSPRNG Cuando se emite un token de restablecimiento de contraseña, envíe ambos valores al usuario, almacene el selector y un hash SHA-256 del token aleatorio en la base de datos. Use el selector para tomar el hash y la ID de usuario, calcule el hash SHA-256 del token que el usuario proporciona con el que está almacenado en la base de datos hash_equals()
.
Código de ejemplo
Generando un token de reinicio en PHP 7 (o 5.6 con random_compat) con 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')
]);
Verificación del token de restablecimiento proporcionado por el usuario:
$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.
}
Estos fragmentos de código no son soluciones completas (evité la validación de entrada y las integraciones del marco), pero deberían servir como ejemplo de qué hacer.