私の Web アプリケーションは、ユーザーがログインするとセッションを使用してそのユーザーに関する情報を保存し、ユーザーがアプリ内のページ間を移動するときにその情報を維持します。この特定のアプリケーションでは、ユーザーのuser_id
、first_name
およびを保存しています。last_name
ログイン時に「ログイン状態を維持する」オプションを提供し、ユーザーのマシンに 2 週間 Cookie を保存し、ユーザーがアプリに戻ったときに同じ詳細でセッションを再開できるようにしたいと考えています。
これを実行するための最善のアプローチは何ですか? クッキーに保存したくありませんuser_id
。そうすると、あるユーザーが別のユーザーの ID を偽造することが容易になると思われるからです。
ベストアンサー1
分かりやすく言うと、ユーザーデータ、またはユーザーデータから派生した何かをこの目的で Cookie に保存している場合、何かが間違っています。
はい、言いました。これで実際の答えに移ることができます。
ユーザー データをハッシュ化することの何が問題なのか、と疑問に思うかもしれません。それは、露出面と隠蔽によるセキュリティの問題です。
ちょっと、自分が攻撃者だと想像してみてください。セッションの remember-me に暗号化クッキーが設定されているのがわかります。これは 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 秒あたり 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 つの部分に分けて保存することです。
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()
ランダムライブラリまたはランダム互換性、または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;
}