Wcześniejsza wersja zaakceptowanej odpowiedzi ( md5(uniqid(mt_rand(), true))
) jest niepewna i oferuje tylko około 2 ^ 60 możliwych wyników - dobrze w zakresie przeszukiwania siłą w ciągu około tygodnia dla napastnika o niskim budżecie:
Ponieważ 56-bitowy klucz DES można wymusić brutalnie w ciągu około 24 godzin , a przeciętny przypadek miałby około 59 bitów entropii, możemy obliczyć 2 ^ 59/2 ^ 56 = około 8 dni. W zależności od tego, jak zaimplementowano tę weryfikację tokenu, może być możliwe praktycznie wyciek informacji o czasie i wywnioskowanie pierwszych N bajtów ważnego tokenu resetowania .
Ponieważ pytanie dotyczy „sprawdzonych metod” i otwiera się…
Chcę wygenerować identyfikator dla zapomnianego hasła
... możemy wywnioskować, że ten token ma niejawne wymagania dotyczące bezpieczeństwa. A kiedy dodajesz wymagania bezpieczeństwa do generatora liczb losowych, najlepszą praktyką jest zawsze używanie generatora liczb pseudolosowych zabezpieczonego kryptograficznie (w skrócie CSPRNG).
Korzystanie z CSPRNG
W PHP 7 możesz użyć bin2hex(random_bytes($n))
(gdzie $n
jest liczbą całkowitą większą niż 15).
W PHP 5 możesz użyć random_compat
tego samego API.
Alternatywnie, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
jeśli ext/mcrypt
zainstalowałeś. Kolejny dobry jednolinijkowy jest bin2hex(openssl_random_pseudo_bytes($n))
.
Oddzielenie wyszukiwania od walidatora
Opierając się na mojej poprzedniej pracy nad bezpiecznymi plikami cookie „zapamiętaj mnie” w PHP , jedynym skutecznym sposobem złagodzenia wspomnianego wycieku czasu (zwykle wprowadzanego przez zapytanie do bazy danych) jest oddzielenie wyszukiwania od weryfikacji.
Jeśli twoja tabela wygląda tak (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)
);
... musisz dodać jeszcze jedną kolumnę selector
, na przykład:
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)
);
Użyj CSPRNG Po wystawieniu tokena resetowania hasła wyślij obie wartości do użytkownika, zapisz selektor i skrót SHA-256 losowego tokenu w bazie danych. Użyj selektora, aby pobrać hash i identyfikator użytkownika, obliczyć skrót SHA-256 tokenu, który podaje użytkownik, z tym, który jest przechowywany w bazie danych hash_equals()
.
Przykładowy kod
Generowanie tokena resetowania w PHP 7 (lub 5.6 z random_compat) z 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'));
$stmt = $pdo->prepare("INSERT INTO account_recovery (userid, selector, token, expires) VALUES (:userid, :selector, :token, :expires);");
$stmt->execute([
'userid' => $userId,
'selector' => $selector,
'token' => hash('sha256', $token),
'expires' => $expires->format('Y-m-d\TH:i:s')
]);
Weryfikacja tokena resetowania podanego przez użytkownika:
$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'])) {
}
}
Te fragmenty kodu nie są kompletnymi rozwiązaniami (zrezygnowałem z walidacji danych wejściowych i integracji ram), ale powinny służyć jako przykład tego, co robić.