La version antérieure de la réponse acceptée ( md5(uniqid(mt_rand(), true))
) n'est pas sécurisée et n'offre qu'environ 2 ^ 60 sorties possibles - bien dans la portée d'une recherche par force brute dans environ une semaine pour un attaquant à petit budget:
Puisqu'une clé DES de 56 bits peut être forcée brutalement en environ 24 heures , et qu'un cas moyen aurait environ 59 bits d'entropie, nous pouvons calculer 2 ^ 59/2 ^ 56 = environ 8 jours. Selon la façon dont cette vérification de jeton est mise en œuvre, il peut être possible de pratiquement perdre des informations de synchronisation et de déduire les N premiers octets d'un jeton de réinitialisation valide .
Puisque la question porte sur les «bonnes pratiques» et s'ouvre sur ...
Je souhaite générer un identifiant pour le mot de passe oublié
... nous pouvons en déduire que ce jeton a des exigences de sécurité implicites. Et lorsque vous ajoutez des exigences de sécurité à un générateur de nombres aléatoires, la meilleure pratique consiste à toujours utiliser un générateur de nombres pseudo-aléatoires cryptographiquement sécurisé (abrégé CSPRNG).
Utilisation d'un CSPRNG
En PHP 7, vous pouvez utiliser bin2hex(random_bytes($n))
(où $n
est un entier supérieur à 15).
En PHP 5, vous pouvez utiliser random_compat
pour exposer la même API.
Sinon, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
si vous avez ext/mcrypt
installé. Un autre bon one-liner est bin2hex(openssl_random_pseudo_bytes($n))
.
Séparer la recherche du validateur
En tirant de mon travail précédent sur les cookies sécurisés «se souvenir de moi» en PHP , le seul moyen efficace d'atténuer la fuite de synchronisation susmentionnée (généralement introduite par la requête de base de données) est de séparer la recherche de la validation.
Si votre table ressemble à ceci (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)
);
... vous devez ajouter une colonne supplémentaire selector
, comme ceci:
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)
);
Utiliser un CSPRNG Lorsqu'un jeton de réinitialisation de mot de passe est émis, envoyez les deux valeurs à l'utilisateur, stockez le sélecteur et un hachage SHA-256 du jeton aléatoire dans la base de données. Utilisez le sélecteur pour récupérer le hachage et l'ID utilisateur, calculez le hachage SHA-256 du jeton fourni par l'utilisateur avec celui stocké dans la base de données hash_equals()
.
Exemple de code
Générer un jeton de réinitialisation en PHP 7 (ou 5.6 avec random_compat) avec 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')
]);
Vérification du jeton de réinitialisation fourni par l'utilisateur:
$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.
}
Ces extraits de code ne sont pas des solutions complètes (j'ai évité la validation d'entrée et les intégrations de framework), mais ils devraient servir d'exemple de ce qu'il faut faire.