1. ホーム
  2. php

[解決済み】「ログインしたままにする」 - 最適な方法とは?

2022-03-30 14:57:58

質問

私のウェブアプリケーションでは、ユーザーがログインした後にその情報を保存し、アプリ内のページを移動する際にもその情報を維持するためにセッションを使用しています。この特定のアプリケーションでは、私は user_id , first_namelast_name の人です。

ログイン時に、ユーザーのマシンに2週間クッキーを保存し、ユーザーがアプリに戻ったときに同じ内容でセッションを再開する、「ログイン状態を保持する」オプションを提供したいのですが、可能ですか?

このような場合、どのような方法があるのでしょうか?私は user_id というのも、あるユーザーが他のユーザーの身元を偽造しようとすることが容易になるように思われるからです。

解決方法は?

この目的のために、ユーザーデータ、またはユーザーデータから派生したものをクッキーに入れるのであれば、何か間違ったことをしていることになります。

ほらね。私はそれを言った。では、実際の答えに移りましょう。

ユーザーデータをハッシュ化することの何が問題なのか、あなたは尋ねたでしょうか?それは、「露出の表面」と「無防備さによるセキュリティ」に尽きます。

あなたが攻撃者だと想像してください。あなたのセッションで、remember-meに設定された暗号クッキーを見ます。それは32文字の幅です。おや。これはMD5かもしれない...

また、あなたが使用したアルゴリズムを相手が知っていることをちょっと想像してみましょう。例えば

md5(salt+username+ip+salt)

これで、攻撃者は、quot;salt"をブルートフォース(実際はソルトではありませんが、これについては後述します)すれば、自分のIPアドレスの任意のユーザ名で好きなだけ偽トークンを生成できるようになりました!このように、攻撃者は、自分のIPアドレスの任意のユーザ名で好きなだけ偽トークンを生成できます。しかし、ソルトをブルートフォースするのは難しいでしょう? もちろんです。しかし、現代のGPUはそれを非常に得意としています。そして、ソルトに十分なランダム性を持たせない限り(十分に大きくしない限り)、ソルトはすぐに破壊され、あなたの城の鍵も一緒に破壊されてしまうでしょう。

要するに、あなたを守ってくれるのは塩だけで、実はあなたが思っているほど守ってくれてはいないのです。

でも、待てよ!?

これらはすべて、攻撃者がアルゴリズムを知っていることを前提にしています。秘密で紛らわしいなら、安全でしょう? 間違い . その考え方には、名前がついています。 無防備さによるセキュリティ ということです。 決して に依存することになります。

より良い方法

より良い方法は、id以外のユーザーの情報をサーバーから絶対に出さないことです。

ユーザーがログインしたときに、大きな(128から256ビット)ランダムなトークンを生成します。そのトークンをユーザーIDにマップするデータベース・テーブルに追加し、クッキーでクライアントに送信します。

もし、攻撃者が他のユーザーのランダムトークンを推測してしまったら?

さて、ここで少し計算してみましょう。私たちは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つのパーツでクッキーを保存することです。

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 または ランダムコンパット または mcrypt_create_iv()DEV_URANDOM )...

hash_equals() を防ぐためのものです。 タイミングアタック . PHP 5.6 未満のバージョンを使用している場合、関数 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;
}