受け入れられた回答 ( md5(uniqid(mt_rand(), true))
) の以前のバージョンは安全ではなく、約 2^60 の可能な出力しか提供しません。これは、低予算の攻撃者が約 1 週間でブルート フォース検索を実行できる範囲内です。
56 ビットの DES キーは約 24 時間でブルート フォースされる可能性があり、平均的なケースでは約 59 ビットのエントロピーがあるため、2^59 / 2^56 = 約 8 日と計算できます。このトークン検証の実装方法によっては、実際にタイミング情報が漏洩し、有効なリセット トークンの最初の N バイトを推測できる可能性があります。
質問は「ベストプラクティス」に関するものであり、次で始まります...
パスワードを忘れた場合の識別子を生成したい
...このトークンには暗黙のセキュリティ要件があると推測できます。また、乱数ジェネレーターにセキュリティ要件を追加する場合のベスト プラクティスは、常に暗号学的に安全な疑似乱数ジェネレーター(略して CSPRNG) を使用することです。
CSPRNG の使用
bin2hex(random_bytes($n))
PHP 7 では、 ($n
は 15 より大きい整数)を使用できます。
PHP 5 では、 を使用random_compat
して同じ API を公開できます。
または、インストールbin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
している場合。ext/mcrypt
もう 1 つの優れたワンライナーはbin2hex(openssl_random_pseudo_bytes($n))
.
ルックアップをバリデーターから分離する
PHP の安全な「remember me」Cookieに関する私の以前の研究から引き出された、前述のタイミング リーク (通常はデータベース クエリによって導入される) を軽減する唯一の効果的な方法は、ルックアップを検証から分離することです。
テーブルが次のようになっている場合 (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
... 次のように、もう 1 つの列を追加する必要があります。
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 ハッシュをデータベースに保存します。セレクターを使用してハッシュとユーザー ID を取得し、ユーザーが提供するトークンの SHA-256 ハッシュを計算しますhash_equals()
。
サンプルコード
PDO を使用して PHP 7 (または random_compat を使用する 5.6) でリセット トークンを生成する:
$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.
}
これらのコード スニペットは完全なソリューションではありませんが (入力の検証とフレームワークの統合は避けました)、何をすべきかの例として役立つはずです。