Более ранняя версия принятого ответа ( 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'));
$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')
]);
Проверка токена сброса, предоставленного пользователем:
$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'])) {
}
}
Эти фрагменты кода не являются законченными решениями (я отказался от проверки ввода и интеграции фреймворка), но они должны служить примером того, что делать.