【入門編】 ログイン試行のレートリミットとブルートフォース対策 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

こんにちは!Webアプリケーションの開発やインフラの管理、本当にお疲れ様です。

システムを作っていると、「ログイン画面に何度も間違ったパスワードを入力されて、不正アクセスされたらどうしよう…」と不安になることってありませんよね?今回は、そんなログイン画面のセキュリティの要(かなめ)である「レートリミット(回数制限)」と「アカウントロックアウト」について、身近な防犯の例えを交えながら、一歩ずつ優しく紐解いていきたいと思います。

難しい言葉が出てきても大丈夫です。一緒にしっかり対策のコツを掴んでいきましょう!

—

1. 泥棒から家を守る!ログイン画面の防犯対策

まずは、私たちが普段暮らしている「お家の鍵」を想像してみてください。

もし、泥棒があなたの家の鍵を開けようと、ありとあらゆる鍵の束をガチャガチャと試してきちゃったらどうしますか?1回や2回ならまだしも、1秒間に100回のスピードで鍵穴に差し込み続けられたら、あっという間に鍵が開いて家に侵入されてしまいますよね。

これが、インターネットの世界における「ブルートフォース攻撃(総当たり攻撃)」です。攻撃者は専用のプログラムを使って、何万回・何百万回ものパスワードの組み合わせを自動でログイン画面に送り込みます。

家の鍵に例える「レートリミット」と「アカウントロックアウト」

この泥棒の攻撃からシステムを守るために、私たちは2つの強力な「防犯システム」を導入する必要があります。

1. IPベースのレートリミット(時間制限付きのインターホン)

  • 「この怪しいインターホン(IPアドレス)からは、1分間に5回以上鳴らしたら一時的に無視するぞ」という仕組みです。自動化された高速なアタックをその場で足止めします。

2. アカウントロックアウト(合鍵の没収)

  • 「特定の部屋の鍵(アカウント)に対して、3回連続で間違ったパスワードを入力したら、正解が分かるまでその鍵自体を一旦使えなくする(ロックする)」という仕組みです。泥棒が特定のあなたのアカウントを執拗に狙うのを防ぎます。

—

2. 実装してみよう!PHPで学ぶシンプルなレートリミット

「概念は分かったけれど、実際にどうやってコードを書けばいいの?」という新人開発者の方に向けて、今回はPHPを使った非常にシンプルなレートリミット(回数制限)のサンプルコードを用意しました。

実際の現場ではRedisやMemcachedといった高速なメモリデータベースを使うことが多いですが、今回は仕組みを理解しやすくするために、セッション(一時的な保存場所)を使った例で見ていきましょう。

<?php
// セッションの開始(ユーザーごとの一時データを管理します)
session_start();

// 設定値
$max_attempts = 3;       // 許容する最大失敗回数
$lockout_time = 60;      // ロックアウトする時間(秒)

// 現在の時刻を取得
$current_time = time();

// セッションに失敗回数やロック時間が未定義の場合は初期化
if (!isset($_SESSION['login_attempts'])) {
    $_SESSION['login_attempts'] = 0;
    $_SESSION['lockout_until'] = 0;
}

// 1. 現在ロックアウト中かどうかをチェック
if ($_SESSION['lockout_until'] > $current_time) {
    $remaining_time = $_SESSION['lockout_until'] - $current_time;
    // まだロックが解除されていない場合は処理を中断
    die("セキュリティ保護のため、アカウントが一時的にロックされています。あと " . $remaining_time . " 秒後にお試しください。");
}

// 2. ログインボタンが押されたときの処理
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $input_username = $_POST['username'] ?? '';
    $input_password = $_POST['password'] ?? '';

    // ここでは簡易的に「user / secret123」を正しい認証情報とします
    $is_success = ($input_username === 'user' && $input_password === 'secret123');

    if ($is_success) {
        // ログイン成功!失敗回数とロック情報をリセット
        $_SESSION['login_attempts'] = 0;
        $_SESSION['lockout_until'] = 0;
        echo "ログインに成功しました!ようこそ!";
    } else {
        // ログイン失敗:失敗回数を1回増やす
        $_SESSION['login_attempts']++;
        
        // 許容回数を超えたかチェック
        if ($_SESSION['login_attempts'] >= $max_attempts) {
            // ロックアウト発動!現在時刻から一定時間先までロック
            $_SESSION['lockout_until'] = $current_time + $lockout_time;
            $_SESSION['login_attempts'] = 0; // カウントをリセット
            
            echo "パスワードの入力を連続で失敗したため、アカウントを一時ロックしました。";
        } else {
            $left_tries = $max_attempts - $_SESSION['login_attempts'];
            echo "ユーザー名またはパスワードが間違っています。あと " . $left_tries . " 回失敗するとロックされます。";
        }
    }
}
?>

<!-- 3. ログイン画面のHTMLフォーム -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ログイン画面</title>
</head>
<body>
    <h2>ログイン</h2>
    <form method="POST" action="">
        <div>
            <label>ユーザー名:</label>
            <input type="text" name="username" required>
        </div>
        <div>
            <label>パスワード:</label>
            <input type="password" name="password" required>
        </div>
        <button type="submit">ログイン</button>
    </form>
</body>
</html>

このコードでは、パスワードを3回連続で間違えると、セッションの仕組みを利用して60秒間ログインの試行をブロックするようになっています。実務ではこれをデータベースやキャッシュサーバーに保存し、IPアドレスやユーザーID単位で管理することになります。

—

3. Webサーバーやクラウドを守る「防犯ヘッダー」とリバースプロキシ

さて、アプリケーションのコードレベルでレートリミットをかけることも大切ですが、それだけでは実は不十分なケースがあります。

なぜなら、攻撃者は1つのIPアドレスからだけでなく、世界中にある何万台もの乗っ取りパソコン(ボットネット)を操って、IPアドレスをコロコロ変えながら攻撃してくるからです(分散ブルートフォース攻撃)。

ここで活躍するのが、Webサーバーの手前にあるリバースプロキシ(NginxやCloudflareなどのCDN)です。

リバースプロキシが受け取る「プロキシヘッダー」

ユーザーがアクセスしてきた際、Nginxなどのプロキシサーバーは、背後にあるアプリケーションサーバー(PHPやNode.jsなど)に対して「本当はこのユーザーからアクセスが来ているよ」と伝えるために、いくつかの特別なHTTPヘッダーを付与します。

  • X-Forwarded-For: アクセス元の本来のIPアドレスを保持するヘッダーです。
  • X-Real-IP: プロキシが直接通信した相手のIPアドレスを伝えます。

例えば、Nginxの設定ファイル(nginx.conf)では、以下のように設定して特定のIPアドレスからのリクエスト頻度を制限(レートリミット)することができます。

# 1秒間に1回までのリクエストを許可し、バースト(一時的な急増)を5回まで許容する設定
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=1r/s;

server {
    listen 80;
    server_name example.com;

    location /login.php {
        # 定義したレートリミットを適用し、超過した場合はバーストを許可せずすぐに弾く
        limit_req zone=login_limit burst=5 nodelay;
        
        # アプリケーションサーバーへ転送
        proxy_pass http://localhost:8080;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

このように、アプリケーションの手前(インフラ層)で高速なアタックを弾くことで、サーバー自体が重くなってダウンしてしまう(DoS状態)リスクを防ぐことができます。

—

4. 異常検知のログパターンを知る

セキュリティ対策で最も重要なのは、「今、自分のシステムが狙われているかどうか」にいち早く気づくことです。ログ(システムの足跡)を監視する際、攻撃者は以下のような特徴的なパターンを残す傾向があります。

1. 同一IPからの短時間の大量アクセス

  • 例:192.0.2.1 というIPから、1分間に200回も /login.php にアクセスが集中している。

2. 多種多様なユーザー名に対する総当たり

  • 例:admin, test, root, user1, guest のように、存在するかどうか分からないユーザー名を総当たりで次々に試している。

3. HTTPステータスコードの偏り

  • 正常なユーザーであれば「ログイン成功(200)」や「フォームの表示(200)」が混ざりますが、攻撃の場合は認証失敗を示すレスポンス(リダイレクトを伴う 302 や、バリデーションエラーの 401 / 403)が不自然なほど連続します。

こうしたログパターンを監視ツール(DatadogやAWS CloudWatch、ELKスタックなど)でキャッチし、異常があればSlackやメールでアラートが飛ぶように仕組みを整えておきましょう。

—

まとめ

いかがでしたでしょうか?今回は、ログイン画面のレートリミットとアカウントロックアウトについて、お家の防犯に例えながら解説しました。

  • レートリミットで、自動化された高速なアタックのスピードを落とす。
  • アカウントロックアウトで、執拗なパスワード推測から特定のアカウントをガッチリ守る。
  • インフラ層(NginxやCDN)もうまく活用して、サーバー全体の負荷もケアする。

セキュリティの対策に「これで完璧!」というゴールはありませんが、こうした基本の「鍵と柵」をしっかりと取り付けておくだけで、大半の自動化されたサイバー攻撃を防ぐことができるようになります。

一歩ずつ、安全で堅牢なシステム作りを楽しんでいきましょう!応援しています!

コメント

タイトルとURLをコピーしました