web-dev-qa-db-ja.com

PHPでユーザーのログイン試行を調整するにはどうすればよいですか?

私はちょうどこの投稿を読んでいました フォームベースのウェブサイト認証の決定的なガイド 迅速な火災ログイン試行の防止。

ベストプラクティス#1:次のように、失敗した試行の回数とともに増加する短い時間遅延

1回失敗=遅延なし
2回の試行失敗= 2秒の遅延
3回の試行失敗= 4秒の遅延
4回の試行失敗= 8秒の遅延
5回の試行失敗= 16秒の遅延
等。

このスキームを攻撃するDoSは非常に実用的ではありませんが、一方で、遅延が指数関数的に増加するため、潜在的に破壊的です。

PHPのログインシステムにこのようなものをどのように実装できるのでしょうか?

53
JasonDavis

スロットリングを単一のIPまたはユーザー名にチェーンすることでDoS攻撃を単純に防止することはできません。このメソッドを使用して、迅速なログイン試行を実際に防止することさえできません。

なぜ?攻撃は複数のIPとユーザーアカウントに及ぶことができるためスロットル試行をバイパスします。

理想的には、サイト全体で失敗したログイン試行をすべて追跡し、それらをタイムスタンプに関連付けることが理想的であることを他の場所で投稿しました:

CREATE TABLE failed_logins (
    id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(16) NOT NULL,
    ip_address INT(11) UNSIGNED NOT NULL,
    attempted DATETIME NOT NULL,
    INDEX `attempted_idx` (`attempted`)
) engine=InnoDB charset=UTF8;

Ip_addressフィールドに関する簡単なメモ:INET_ATON()およびINET_NTOA()を使用して、それぞれデータを格納および取得できます。これらは、IPアドレスと符号なし整数との変換に本質的に相当します。

# example of insertion
INSERT INTO failed_logins SET username = 'example', ip_address = INET_ATON('192.168.0.1'), attempted = CURRENT_TIMESTAMP;
# example of selection
SELECT id, username, INET_NTOA(ip_address) AS ip_address, attempted;

overall所定の時間(この例では15分)で失敗したログインの数に基づいて、特定の遅延しきい値を決定します。これは、failed_loginsテーブルは時間の経過とともに変化しますユーザーの数と、パスワードをリコール(および入力)できるユーザーの数に基づきます。


> 10 failed attempts = 1 second
> 20 failed attempts = 2 seconds
> 30 failed attempts = reCaptcha

ログインに失敗するたびにテーブルをクエリし、一定の期間(15分など)に失敗したログインの数を見つけます。


SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute);

指定された期間の試行回数が制限を超えている場合は、指定された期間の失敗した試行回数がしきい値未満になるまで、スロットリングを強制するか、すべてのユーザーにキャプチャ(つまりreCaptcha)の使用を強制します。

// array of throttling
$throttle = array(10 => 1, 20 => 2, 30 => 'recaptcha');

// retrieve the latest failed login attempts
$sql = 'SELECT MAX(attempted) AS attempted FROM failed_logins';
$result = mysql_query($sql);
if (mysql_affected_rows($result) > 0) {
    $row = mysql_fetch_assoc($result);

    $latest_attempt = (int) date('U', strtotime($row['attempted']));

    // get the number of failed attempts
    $sql = 'SELECT COUNT(1) AS failed FROM failed_logins WHERE attempted > DATE_SUB(NOW(), INTERVAL 15 minute)';
    $result = mysql_query($sql);
    if (mysql_affected_rows($result) > 0) {
        // get the returned row
        $row = mysql_fetch_assoc($result);
        $failed_attempts = (int) $row['failed'];

        // assume the number of failed attempts was stored in $failed_attempts
        krsort($throttle);
        foreach ($throttle as $attempts => $delay) {
            if ($failed_attempts > $attempts) {
                // we need to throttle based on delay
                if (is_numeric($delay)) {
                    $remaining_delay = time() - $latest_attempt - $delay;
                    // output remaining delay
                    echo 'You must wait ' . $remaining_delay . ' seconds before your next login attempt';
                } else {
                    // code to display recaptcha on login form goes here
                }
                break;
            }
        }        
    }
}

特定のしきい値でreCaptchaを使用すると、複数のフロントからの攻撃が停止され、通常のサイトユーザーは、正当なログイン試行の大幅な遅延を経験しなくなります。

76
Corey Ballou

セッション情報の保存、Cookie情報の保存、またはIP情報の保存の3つの基本的なアプローチがあります。

セッション情報を使用すると、エンドユーザー(攻撃者)が新しいセッションを強制的に呼び出し、戦術をバイパスし、遅滞なく再度ログインできます。セッションの実装は非常に簡単で、ユーザーの最後の既知のログイン時間をセッション変数に保存し、現在の時間と照合して、遅延が十分に長いことを確認します。

Cookieを使用する場合、攻撃者は単純にCookieを拒否できますが、これは実際には実行可能なものではありません。

IPアドレスを追跡する場合、IPアドレスからのログイン試行を何らかの方法で、できればデータベースに保存する必要があります。ユーザーがログオンしようとすると、記録されたIPのリストを更新するだけです。適切な間隔でこのテーブルをパージし、しばらくアクティブになっていないIPアドレスをダンプする必要があります。落とし穴(常に落とし穴があります)は、一部のユーザーがIPアドレスを共有する可能性があり、境界条件では、遅延がユーザーに誤って影響を与える可能性があることです。失敗したログイン、および失敗したログインのみを追跡しているため、これはそれほど苦痛を引き起こさないはずです。

5
Mark Elliot

ログインプロセスでは、ログインの成功と失敗の両方の速度を下げる必要があります。ログイン試行自体は、約1秒より速くなることはありません。そうである場合、ブルートフォースは遅延を使用して、成功が失敗よりも短いため、試行が失敗したことを認識します。その後、1秒あたりにより多くの組み合わせを評価できます。

マシンごとの同時ログイン試行回数は、ロードバランサーによって制限する必要があります。最後に、同じユーザーまたはパスワードが複数のユーザー/パスワードのログイン試行によって再利用されるかどうかを追跡する必要があります。人間は、1分あたり約200単語より速く入力することはできません。そのため、1分あたり200ワードを超える高速または連続ログイン試行は、一連のマシンから行われます。したがって、これらは顧客ではないため、安全にブラックリストにパイプできます。ホストごとのブラックリスト時間は、約1秒を超える必要はありません。これは人間にとって決して不便なことではありませんが、シリアルであろうとパラレルであろうと、総当たり攻撃で大混乱を引き起こします。

1秒あたり1つの組み合わせで2 * 10 ^ 19の組み合わせを、40億の個別のIPアドレスで並行して実行すると、検索スペースとして使い尽くすのに158年かかります。 40億の攻撃者に対してユーザーごとに1日続くには、少なくとも9桁の完全にランダムな英数字のパスワードが必要です。少なくとも13桁、1.7 * 10 ^ 20の組み合わせのパスフレーズでユーザーをトレーニングすることを検討してください。

この遅延により、攻撃者はサイトをブルートフォースするのではなく、パスワードハッシュファイルを盗むようになります。承認済みの名前付きハッシュ手法を使用します。インターネットIPの全人口を1秒間禁止することで、人間が理解するような不便さなしに並列攻撃の影響を制限できます。最後に、システムが1秒で1000回を超えるログオン試行の失敗を許可し、システムを禁止することに何も応答しない場合、セキュリティ計画には大きな問題があります。まず、その自動応答を修正します。

4
Don Turnblade
session_start();
$_SESSION['hit'] += 1; // Only Increase on Failed Attempts
$delays = array(1=>0, 2=>2, 3=>4, 4=>8, 5=>16); // Array of # of Attempts => Secs

sleep($delays[$_SESSION['hit']]); // Sleep for that Duration.

またはCyroが提案したとおり:

sleep(2 ^ (intval($_SESSION['hit']) - 1));

少し荒いですが、基本的なコンポーネントはあります。このページを更新すると、更新するたびに遅延が長くなります。

また、データベースにカウントを保持して、IPによる試行の失敗回数を確認することもできます。 IPに基づいて使用し、データを自分の側に保持することにより、ユーザーが遅延を止めるためにCookieをクリアできないようにします。

基本的に、開始コードは次のとおりです。

$count = get_attempts(); // Get the Number of Attempts

sleep(2 ^ (intval($count) - 1));

function get_attempts()
{
    $result = mysql_query("SELECT FROM TABLE WHERE IP=\"".$_SERVER['REMOTE_ADDR']."\"");
    if(mysql_num_rows($result) > 0)
    {
        $array = mysql_fetch_assoc($array);
        return $array['Hits'];
    }
    else
    {
        return 0;
    }
}
3
Tyler Carter

失敗した試行をIPごとにデータベースに保存します。 (ログインシステムがあるので、これを行う方法をよく知っていると思います。)

明らかに、セッションは魅力的な方法ですが、本当に熱心な人は、スロットルを完全に回避するために、失敗した試行でセッションCookieを単純に削除できることを非常に簡単に理解できます。

ログイン試行時に、最近(たとえば、最後の15分間)ログイン試行が何回あったか、および最新の試行の時刻を取得します。

$failed_attempts = 3; // for example
$latest_attempt = 1263874972; // again, for example
$delay_in_seconds = pow(2, $failed_attempts); // that's 2 to the $failed_attempts power
$remaining_delay = time() - $latest_attempt - $delay_in_seconds;
if($remaining_delay > 0) {
    echo "Wait $remaining_delay more seconds, silly!";
}
3
Matchu

私見、DOS攻撃に対する防御は、PHPコードではなく、Webサーバーレベル(またはネットワークハードウェアでも)で対処する方が適切です。

2
vicatcu

セッションを使用できます。ユーザーがログインに失敗するたびに、試行回数を格納する値を増やします。試行回数から必要な遅延を計算するか、ユーザーがセッションで再試行できる実際の時間を設定することもできます。

より信頼性の高い方法は、その特定のIPアドレスの試行と新しい試行時間をデータベースに保存することです。

2
Sampson

この場合、Cookieまたはセッションベースのメソッドはもちろん役に立ちません。アプリケーションは、以前のログイン試行のIPアドレスまたはタイムスタンプ(または両方)を確認する必要があります。

攻撃者がリクエストを開始するためのIPを複数持っている場合、IPチェックをバイパスできます。また、複数のユーザーが同じIPからサーバーに接続すると、問題が発生する可能性があります。後者の場合、誰かが数回ログインに失敗すると、同じIPを共有する全員が一定期間そのユーザー名でログインできなくなります。

タイムスタンプチェックには、上記と同じ問題があります。複数回試行するだけで、誰もが特定のアカウントにログインできないようにすることができます。おそらく、最後の試行を長時間待つのではなくキャプチャを使用するのが適切な回避策です。

ログインシステムが防止する必要がある唯一の追加事項は、試行チェック機能の競合状態です。たとえば、次の擬似コードでは

$time = get_latest_attempt_timestamp($username);
$attempts = get_latest_attempt_number($username);

if (is_valid_request($time, $attempts)) {
    do_login($username, $password);
} else {
    increment_attempt_number($username);
    display_error($attempts);
}

攻撃者が同時リクエストをログインページに送信するとどうなりますか?おそらくすべてのリクエストは同じ優先度で実行され、他のリクエストが2行目を超える前に、どのリクエストもincrement_attempt_number命令に到達しない可能性があります。したがって、すべてのリクエストは同じ$ timeと$ attemptsの値を取得して実行されます。この種のセキュリティの問題を防ぐことは、複雑なアプリケーションでは困難な場合があり、データベースの一部のテーブル/行のロックとロック解除を伴います。もちろん、アプリケーションの速度は低下します。

1
user225840

簡単な答えは次のとおりです。これをしないでください。総当たり攻撃から身を守ることはできません。状況を悪化させることさえできます。

提案されたソリューションはどれも機能しません。 IPをスロットルのパラメーターとして使用すると、攻撃者は膨大な数のIPに攻撃を広げます。 session(cookie)を使用すると、攻撃者はすべてのcookieをドロップします。あなたが考えることができるすべての合計は、総当たり攻撃者が克服できなかったものは絶対にないということです。

ただし、1つだけあります。ログインしようとしたユーザー名に依存するだけです。そのため、ユーザーがログインして調整しようとした頻度を追跡する他のすべてのパラメーターを見ることはありません。しかし、攻撃者はあなたに危害を加えようとしています。彼がこれを認識すると、彼はユーザー名をブルートフォースします。

これにより、ユーザーがログインしようとすると、ほとんどすべてのユーザーが最大値に調整されます。Webサイトは役に立たなくなります。攻撃者:成功。

一般に、パスワードチェックを約200ミリ秒間遅らせることができます。Webサイトユーザーはほとんど気付かないでしょう。しかし、ブルートフォーサーはそうします。 (繰り返しますが、彼は複数のIPにまたがることができます)ただし、プログラムで実行できないため、ブルートフォースやDDoSからあなたを保護するものはありません。

これを行う唯一の方法は、インフラストラクチャを使用することです。

パスワードをハッシュするためにMD5またはSHA-xの代わりにbcryptを使用する必要があります。これにより、誰かがあなたのデータベースを盗む場合、パスワードの解読がより難しくなります(共有または管理されたホストにいると思います)

失望させて申し訳ありませんが、ここでのすべてのソリューションには弱点があり、バックエンドロジック内でそれらを克服する方法はありません。

1
nico gawenda

上記の説明によると、セッション、Cookie、およびIPアドレスは効果的ではありません-すべてが攻撃者によって操作される可能性があります。

ブルートフォース攻撃を防止したい場合、唯一の実用的な解決策は、指定されたユーザー名に基づいて試行回数を決定することですが、これにより、攻撃者は有効なユーザーのログインをブロックしてサイトをDOSにできることに注意してください.

例えば.

$valid=check_auth($_POST['USERNAME'],$_POST['PASSWD']);
$delay=get_delay($_POST['USERNAME'],$valid);

if (!$valid) {
   header("Location: login.php");
   exit;
}
...
function get_delay($username,$authenticated)
{
    $loginfile=SOME_BASE_DIR . md5($username);
    if (@filemtime($loginfile)<time()-8600) {
       // last login was never or over a day ago
       return 0;
    }
    $attempts=(integer)file_get_contents($loginfile);
    $delay=$attempts ? pow(2,$attempts) : 0;
    $next_value=$authenticated ? 0 : $attempts + 1;
    file_put_contents($loginfile, $next_value);
    sleep($delay); // NB this is done regardless if passwd valid
    // you might want to put in your own garbage collection here
 }

書かれているように、この手順ではセキュリティ情報が漏洩することに注意してください。つまり、システムを攻撃しているユーザーがユーザーがログインしたタイミングを確認できる可能性があります(攻撃者の応答時間は0になります)。また、以前の遅延とファイルのタイムスタンプに基づいて遅延が計算されるようにアルゴリズムを調整することもできます。

HTH

C.

1
symcbean

通常、ログイン履歴とログイン試行テーブルを作成します。試行テーブルは、ユーザー名、パスワード、IPアドレスなどをログに記録します。テーブルに対してクエリを実行して、遅延が必要かどうかを確認します。特定の時間(たとえば1時間)に20回を超える試行に対して完全にブロックすることをお勧めします。

1
sestocker

cballuoはすばらしい答えを提供してくれました。私は、mysqliをサポートする更新バージョンを提供することで、好意を返したかっただけです。 SQLやその他の小さなもののテーブル/フィールド列を少し変更しましたが、mysqliと同等のものをお探しの方に役立つはずです。

function get_multiple_rows($result) {
  $rows = array();
  while($row = $result->fetch_assoc()) {
    $rows[] = $row;
  }
  return $rows;
}

$throttle = array(10 => 1, 20 => 2, 30 => 5);

$query = "SELECT MAX(time) AS attempted FROM failed_logins";    

if ($result = $mysqli->query($query)) {

    $rows = get_multiple_rows($result);

$result->free();

$latest_attempt = (int) date('U', strtotime($rows[0]['attempted'])); 

$query = "SELECT COUNT(1) AS failed FROM failed_logins WHERE time > DATE_SUB(NOW(), 
INTERVAL 15 minute)";   

if ($result = $mysqli->query($query)) {

$rows = get_multiple_rows($result);

$result->free();

    $failed_attempts = (int) $rows[0]['failed'];

    krsort($throttle);
    foreach ($throttle as $attempts => $delay) {
        if ($failed_attempts > $attempts) {
                echo $failed_attempts;
                $remaining_delay = (time() - $latest_attempt) - $delay;

                if ($remaining_delay < 0) {
                echo 'You must wait ' . abs($remaining_delay) . ' seconds before your next login attempt';
                }                

            break;
        }
     }        
  }
}
0
jason328