Попередня версія прийнятої відповіді ( md5(uniqid(mt_rand(), true))
) є небезпечною і пропонує лише близько 2 ^ 60 можливих результатів - що знаходиться в межах обсягу грубої сили приблизно за тиждень для малобюджетного зловмисника:
Оскільки 56-розрядний ключ DES може бути примусово застосований приблизно за 24 години , а середній випадок матиме близько 59 біт ентропії, ми можемо розрахувати 2 ^ 59/2 ^ 56 = приблизно 8 днів. Залежно від того, як реалізована перевірка цього маркера, можливо, можливо практично виточити інформацію про синхронізацію та зробити перші N байт дійсного маркера скидання .
Оскільки питання стосується "найкращих практик" і відкривається ...
Я хочу сформувати ідентифікатор забутого пароля
... ми можемо зробити висновок, що цей маркер має неявні вимоги до безпеки. І коли ви додаєте вимоги безпеки до генератора випадкових чисел, найкращою практикою є завжди використовувати криптографічно захищений генератор псевдовипадкових чисел (скорочено CSPRNG).
Використання CSPRNG
У PHP 7 ви можете використовувати bin2hex(random_bytes($n))
(де$n
ціле число більше 15).
У PHP 5 ви можете використовувати random_compat
для викриття той самий API.
Як варіант, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
якщо ви ext/mcrypt
встановили. Ще одним хорошим одноклассником єbin2hex(openssl_random_pseudo_bytes($n))
.
Відокремлення пошуку від валідатора
Виходячи з моєї попередньої роботи щодо безпечних файлів cookie "запам'ятай мене" у PHP , єдиним ефективним способом пом'якшення згаданого витоку часу (як правило, запровадженого запитом до бази даних) є відокремлення пошуку від перевірки.
Якщо ваша таблиця виглядає так (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)
);
... потрібно додати ще один стовпець selector
, приблизно так:
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)
);
Використовуйте CSPRNG Коли видано маркер скидання пароля, надішліть обидва значення користувачеві, збережіть селектор і хеш SHA-256 випадкового маркера в базі даних. За допомогою селектора захопіть хеш та ідентифікатор користувача, обчисліть хеш SHA-256 токена, який користувач надає, із хешем, що зберігається у базі даних hash_equals()
.
Приклад коду
Створення маркера скидання в PHP 7 (або 5.6 із random_compat) за допомогою 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')
]);
Перевірка наданого користувачем маркера скидання:
$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.
}
Ці фрагменти коду не є повними рішеннями (я уникнув перевірки вводу та інтеграції фреймворку), але вони повинні слугувати прикладом того, що робити.