Die frühere Version der akzeptierten Antwort ( md5(uniqid(mt_rand(), true))
) ist unsicher und bietet nur etwa 2 ^ 60 mögliche Ausgaben - im Bereich einer Brute-Force-Suche in etwa einer Woche für einen Low-Budget-Angreifer:
Da ein 56-Bit-DES-Schlüssel in ungefähr 24 Stunden brutal erzwungen werden kann und ein durchschnittlicher Fall ungefähr 59 Bit Entropie haben würde, können wir 2 ^ 59/2 ^ 56 = ungefähr 8 Tage berechnen. Abhängig davon, wie diese Token-Überprüfung implementiert ist, kann es möglich sein, Zeitinformationen praktisch zu verlieren und auf die ersten N Bytes eines gültigen Reset-Tokens zu schließen .
Da es sich bei der Frage um "Best Practices" handelt und mit ...
Ich möchte eine Kennung für ein vergessenes Passwort generieren
... können wir daraus schließen, dass dieses Token implizite Sicherheitsanforderungen hat. Wenn Sie einem Zufallszahlengenerator Sicherheitsanforderungen hinzufügen, empfiehlt es sich, immer einen kryptografisch sicheren Pseudozufallszahlengenerator (abgekürzt CSPRNG) zu verwenden.
Verwenden eines CSPRNG
In PHP 7 können Sie verwenden bin2hex(random_bytes($n))
(wobei $n
eine Ganzzahl größer als 15 ist).
In PHP 5 können Sie random_compat
dieselbe API verfügbar machen.
Alternativ, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
wenn Sie ext/mcrypt
installiert haben. Ein weiterer guter Einzeiler ist bin2hex(openssl_random_pseudo_bytes($n))
.
Trennen der Suche vom Validator
Aus meiner früheren Arbeit an sicheren "Remember Me" -Cookies in PHP besteht die einzige wirksame Möglichkeit, das oben erwähnte Timing-Leck (das normalerweise durch die Datenbankabfrage verursacht wird) zu verringern, darin, die Suche von der Validierung zu trennen.
Wenn Ihre Tabelle so aussieht (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)
);
... Sie müssen eine weitere Spalte hinzufügen selector
, wie folgt:
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)
);
Verwenden eines CSPRNG Wenn ein Token zum Zurücksetzen des Kennworts ausgegeben wird, senden Sie beide Werte an den Benutzer, speichern Sie den Selektor und einen SHA-256-Hash des zufälligen Tokens in der Datenbank. Verwenden Sie den Selektor, um den Hash und die Benutzer-ID abzurufen, und berechnen Sie den SHA-256-Hash des Tokens, den der Benutzer mit dem in der Datenbank gespeicherten Token bereitstellt hash_equals()
.
Beispielcode
Generieren eines Reset-Tokens in PHP 7 (oder 5.6 mit random_compat) mit 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')
]);
Überprüfen des vom Benutzer bereitgestellten Reset-Tokens:
$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.
}
Diese Codefragmente sind keine vollständigen Lösungen (ich habe die Eingabevalidierung und die Framework-Integration vermieden), aber sie sollten als Beispiel dafür dienen, was zu tun ist.