わかりました、率直に言ってみましょう。ユーザーデータ、またはユーザーデータから派生したものをこの目的のためにCookieに入れている場合は、何か間違っています。
そこ。私はそれを言った。それでは、実際の答えに移りましょう。
ユーザーデータのハッシュの何が問題になっていますか?まあ、それは露出面とあいまいさによるセキュリティに帰着します。
あなたが攻撃者だと一瞬想像してみてください。セッションで、remember-meに設定された暗号化Cookieが表示されます。幅は32文字です。ああ。それはMD5かもしれません...
彼らがあなたが使ったアルゴリズムを知っていることを一瞬想像してみましょう。例えば:
md5(salt+username+ip+salt)
これで、攻撃者が行う必要があるのは、「塩」をブルートフォースすることです(これは実際には塩ではありませんが、後で詳しく説明します)。IPアドレスのユーザー名を使用して、必要なすべての偽のトークンを生成できます。しかし、ブルートフォースをかけるのは難しいですよね?もちろんです。しかし、現代のGPUは非常に優れています。そして、その中で十分なランダム性を使用しない限り(十分に大きくする)、それはすぐに落下し、それに伴って城の鍵となります。
要するに、あなたを守る唯一のものはあなたが思っているほど実際にあなたを守っていない塩です。
ちょっと待って!
これらはすべて、攻撃者がアルゴリズムを知っていることを前提としています!それが秘密で混乱しているなら、あなたは安全ですよね?間違っています。その考え方には名前があります:Security Through Obscurity。これは決して信頼されるべきではありません。
より良い方法
より良い方法は、IDを除いて、ユーザーの情報がサーバーから出ないようにすることです。
ユーザーがログインするときに、大きな(128〜256ビット)ランダムトークンを生成します。これを、トークンをユーザーIDにマップするデータベーステーブルに追加し、Cookieでクライアントに送信します。
攻撃者が別のユーザーのランダムトークンを推測した場合はどうなりますか?
さて、ここで数学をしましょう。128ビットのランダムトークンを生成しています。つまり、
possibilities = 2^128
possibilities = 3.4 * 10^38
さて、その数がいかに驚くほど大きいかを示すために、インターネット上のすべてのサーバー(今日は50,000,000としましょう)がその数を毎秒1,000,000,000の割合でブルートフォースにしようとしているとしましょう。実際には、サーバーはそのような負荷の下で溶けるでしょうが、これを試してみましょう。
guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000
つまり、1秒あたり50兆回の推測です。それは速いです!正しい?
time_to_guess = possibilities / guesses_per_second
time_to_guess = 3.4e38 / 50,000,000,000,000,000
time_to_guess = 6,800,000,000,000,000,000,000
6.8セクシオン秒...
それをもっと親しみやすい数字にしてみましょう。
215,626,585,489,599 years
またはさらに良い:
47917 times the age of the universe
はい、それは宇宙の時代の47917倍です...
基本的に、それはクラックされないでしょう。
要約すると:
私が推奨するより良いアプローチは、3つの部分でCookieを保存することです。
function onLogin($user) {
$token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit
storeTokenForUser($user, $token);
$cookie = $user . ':' . $token;
$mac = hash_hmac('sha256', $cookie, SECRET_KEY);
$cookie .= ':' . $mac;
setcookie('rememberme', $cookie);
}
次に、検証するには:
function rememberMe() {
$cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : '';
if ($cookie) {
list ($user, $token, $mac) = explode(':', $cookie);
if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) {
return false;
}
$usertoken = fetchTokenByUserName($user);
if (hash_equals($usertoken, $token)) {
logUserIn($user);
}
}
}
注:トークンまたはユーザーとトークンの組み合わせを使用して、データベース内のレコードを検索しないでください。常にユーザーに基づいてレコードをフェッチし、タイミングセーフな比較関数を使用して、フェッチしたトークンを後で比較するようにしてください。タイミング攻撃の詳細。
さて、それが暗号の秘密であることは非常に重要ですSECRET_KEY
(/dev/urandom
高エントロピー入力のようなものによって生成された、および/または高エントロピー入力から派生した)。また、GenerateRandomToken()
強力なランダムソースである必要があります(mt_rand()
十分に強力ではありません。RandomLibやrandom_compatなどのライブラリを使用するかmcrypt_create_iv()
、DEV_URANDOM
)で...
hash_equals()
ないようにすることであるタイミング攻撃を。PHP 5.6より前のバージョンのPHPを使用する場合、この機能hash_equals()
はサポートされません。この場合hash_equals()
、timingSafeCompare関数で置き換えることができます。
/**
* A timing safe equals comparison
*
* To prevent leaking length information, it is important
* that user input is always used as the second parameter.
*
* @param string $safe The internal (safe) value to be checked
* @param string $user The user submitted (unsafe) value
*
* @return boolean True if the two strings are identical.
*/
function timingSafeCompare($safe, $user) {
if (function_exists('hash_equals')) {
return hash_equals($safe, $user); // PHP 5.6
}
// Prevent issues if string length is 0
$safe .= chr(0);
$user .= chr(0);
// mbstring.func_overload can make strlen() return invalid numbers
// when operating on raw binary strings; force an 8bit charset here:
if (function_exists('mb_strlen')) {
$safeLen = mb_strlen($safe, '8bit');
$userLen = mb_strlen($user, '8bit');
} else {
$safeLen = strlen($safe);
$userLen = strlen($user);
}
// Set the result to the difference between the lengths
$result = $safeLen - $userLen;
// Note that we ALWAYS iterate over the user-supplied length
// This is to prevent leaking length information
for ($i = 0; $i < $userLen; $i++) {
// Using % here is a trick to prevent notices
// It's safe, since if the lengths are different
// $result is already non-0
$result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
}
// They are only identical strings if $result is exactly 0...
return $result === 0;
}