Versi awal dari jawaban yang diterima ( md5(uniqid(mt_rand(), true))
) tidak aman dan hanya menawarkan sekitar 2 ^ 60 kemungkinan keluaran - baik dalam kisaran pencarian brute force dalam waktu sekitar satu minggu untuk penyerang anggaran rendah:
Karena kunci DES 56-bit dapat dipaksakan secara brutal dalam waktu sekitar 24 jam , dan kasus rata-rata akan memiliki sekitar 59 bit entropi, kita dapat menghitung 2 ^ 59/2 ^ 56 = sekitar 8 hari. Bergantung pada bagaimana verifikasi token ini diimplementasikan, dimungkinkan untuk secara praktis membocorkan informasi pengaturan waktu dan menyimpulkan N byte pertama dari token reset yang valid .
Karena pertanyaannya adalah tentang "praktik terbaik" dan dibuka dengan ...
Saya ingin membuat pengenal untuk kata sandi yang lupa
... kita dapat menyimpulkan bahwa token ini memiliki persyaratan keamanan implisit. Dan saat Anda menambahkan persyaratan keamanan ke pembuat nomor acak, praktik terbaiknya adalah selalu menggunakan pembuat nomor pseudorandom yang aman secara kriptografis (disingkat CSPRNG).
Menggunakan CSPRNG
Di PHP 7, Anda dapat menggunakan bin2hex(random_bytes($n))
(di mana $n
bilangan bulat lebih besar dari 15).
Di PHP 5, Anda dapat menggunakan random_compat
untuk mengekspos API yang sama.
Atau, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
jika Anda telah ext/mcrypt
menginstal. Satu baris bagus lainnya adalah bin2hex(openssl_random_pseudo_bytes($n))
.
Memisahkan Pencarian dari Validator
Menarik dari pekerjaan saya sebelumnya tentang cookie "ingat saya" yang aman di PHP , satu-satunya cara efektif untuk mengurangi kebocoran waktu yang disebutkan di atas (biasanya diperkenalkan oleh kueri database) adalah dengan memisahkan pencarian dari validasi.
Jika tabel Anda terlihat seperti ini (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)
);
... Anda perlu menambahkan satu kolom lagi selector
, seperti ini:
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)
);
Gunakan CSPRNG Saat token setel ulang sandi diterbitkan, kirim kedua nilai kepada pengguna, simpan pemilih, dan hash SHA-256 dari token acak dalam database. Gunakan selektor untuk mengambil hash dan ID Pengguna, hitung hash SHA-256 dari token yang diberikan pengguna dengan yang disimpan dalam database menggunakan hash_equals()
.
Kode Contoh
Menghasilkan token reset di PHP 7 (atau 5.6 dengan random_compat) dengan 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')
]);
Memverifikasi token setel ulang yang disediakan pengguna:
$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.
}
Potongan kode ini bukanlah solusi lengkap (saya menghindari validasi input dan integrasi kerangka kerja), tetapi mereka harus berfungsi sebagai contoh tentang apa yang harus dilakukan.