La versione precedente della risposta accettata ( md5(uniqid(mt_rand(), true))
) è insicura e offre solo circa 2 ^ 60 possibili output, ben all'interno dell'intervallo di una ricerca di forza bruta in circa una settimana per un attaccante a basso budget:
Poiché una chiave DES a 56 bit può essere forzata in circa 24 ore e un caso medio avrebbe circa 59 bit di entropia, possiamo calcolare 2 ^ 59/2 ^ 56 = circa 8 giorni. A seconda di come viene implementata questa verifica del token, potrebbe essere possibile praticamente perdere informazioni sulla temporizzazione e dedurre i primi N byte di un token di ripristino valido .
Poiché la domanda riguarda le "migliori pratiche" e si apre con ...
Voglio generare l'identificatore per la password dimenticata
... possiamo dedurre che questo token ha requisiti di sicurezza impliciti. E quando si aggiungono requisiti di sicurezza a un generatore di numeri casuali, la migliore pratica è quella di utilizzare sempre un generatore di numeri pseudocasuali crittograficamente sicuro (abbreviato CSPRNG).
Utilizzando un CSPRNG
In PHP 7, puoi usare bin2hex(random_bytes($n))
(dove $n
è un numero intero maggiore di 15).
In PHP 5, puoi utilizzare random_compat
per esporre la stessa API.
In alternativa, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
se hai ext/mcrypt
installato. Un'altra buona battuta è bin2hex(openssl_random_pseudo_bytes($n))
.
Separazione della ricerca dal validatore
Prendendo spunto dal mio lavoro precedente sui cookie sicuri "ricordami" in PHP , l'unico modo efficace per mitigare la suddetta perdita di tempo (tipicamente introdotta dalla query del database) è separare la ricerca dalla convalida.
Se la tua tabella ha questo aspetto (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)
);
... devi aggiungere un'altra colonna, in questo selector
modo:
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)
);
Utilizzare un CSPRNG Quando viene emesso un token di reimpostazione della password, inviare entrambi i valori all'utente, memorizzare il selettore e un hash SHA-256 del token casuale nel database. Usa il selettore per prendere l'hash e l'ID utente, calcola l'hash SHA-256 del token che l'utente fornisce con quello memorizzato nel database utilizzando hash_equals()
.
Codice di esempio
Generazione di un token di ripristino in 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')
]);
Verifica del token di ripristino fornito dall'utente:
$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.
}
Questi frammenti di codice non sono soluzioni complete (ho evitato la convalida dell'input e le integrazioni del framework), ma dovrebbero servire come esempio di cosa fare.