パスワードを忘れた場合の識別子を生成したい。私はmt_Rand()でタイムスタンプを使用してそれを行うことができると読みましたが、一部の人々はタイムスタンプが毎回一意ではないかもしれないと言っています。だから私はここで少し混乱しています。これでタイムスタンプを使用してそれを行うことはできますか?
質問
カスタムの長さのランダム/ユニークトークンを生成するベストプラクティスは何ですか?
ここではたくさんの質問がありますが、さまざまな人からさまざまな意見を読んだ後、私はもっと混乱しています。
PHPでは、 random_bytes()
を使用します。理由:パスワードリマインダートークンを取得する方法を模索しており、1回限りのログイン資格情報である場合、実際に保護するデータがあります(ユーザーアカウント全体)
したがって、コードは次のようになります。
//$length = 78 etc
$token = bin2hex(random_bytes($length));
Update:以前のバージョン この答えはuniqid()
を参照しており、存在する場合は正しくない一意性だけでなく、セキュリティの問題。 uniqid()
は、基本的に、何らかのエンコードを備えたmicrotime()
です。サーバー上のmicrotime()
の正確な予測を取得する簡単な方法があります。攻撃者はパスワードリセットリクエストを発行してから、可能性のあるトークンをいくつか試すことができます。追加のエントロピーも同様に弱いため、more_entropyを使用する場合にもこれが可能です。これを指摘してくれた @ NikiC と @ ScottArciszewski に感謝します。
詳細については
これは、「ランダムな」要求に答えます。
アディの答え1 Security.StackExchangeにはこれに対するソリューションがあります。
OpenSSLをサポートしていることを確認してください。このワンライナーで問題が発生することはありません。
$token = bin2hex(openssl_random_pseudo_bytes(16));
1. Adi、2018年11月12日、Celeritas、「確認メール用の推測できないトークンの生成」、13年9月20日7:06、 https://security.stackexchange.com/a/40314/
受け入れられた回答(md5(uniqid(mt_Rand(), true))
)の以前のバージョンは安全ではなく、約2 ^ 60の出力しか提供していません。低予算の攻撃者にとっては、約1週間でブルートフォース検索の範囲内です。
mt_Rand()
は予測可能です (最大31ビットのエントロピーしか追加できません)uniqid()
は最大29ビットのエントロピーのみを追加しますmd5()
はエントロピーを追加せず、決定論的に混合します56ビットDESキーは約24時間で総当たり攻撃できる であり、平均的なケースでは約59ビットのエントロピーがあるため、2 ^ 59/2を計算できます。 ^ 56 =約8日間。このトークン検証の実装方法に応じて、 タイミング情報を実際にリークし、有効なリセットトークンの最初のNバイトを推測できる可能性があります 。
質問は「ベストプラクティス」に関するものであり、...
パスワードを忘れた場合の識別子を生成したい
...このトークンには暗黙的なセキュリティ要件があると推測できます。乱数ジェネレーターにセキュリティ要件を追加するときのベストプラクティスは、常に暗号的に安全な擬似乱数ジェネレーター(略称CSPRNG)を使用することです。
PHP 7では、bin2hex(random_bytes($n))
を使用できます($n
は15より大きい整数です)。
PHP 5では、 random_compat
を使用して同じAPIを公開できます。
または、ext/mcrypt
がインストールされている場合は、bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM))
。もう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)
);
...もう1つの列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ハッシュをデータベースに保存します。セレクタを使用してハッシュとユーザー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.
}
これらのコードスニペットは完全なソリューションではありません(入力の検証とフレームワークの統合を避けました)が、何をすべきかの例として役立つはずです。
128 =生成されたトークンの長さの1/2であるDEV_RANDOMも使用できます。以下のコードは256トークンを生成します。
$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
これは、非常にランダムなトークンが必要な場合に役立ちます
<?php
echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>