0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【第14回】管理者ログインを守り抜け!パスワード試行制限と15分間ロックアウトによるブルートフォース防御

0
Posted at

「毎秒50回のペースで『password』『admin』『123456』を試すログインリクエストが押し寄せていた」
令和に自作した2ちゃんねる風掲示板「つゔぁいちゃんねる(2ch.biz)」開発秘話の第14弾。今回は、サイト全体の全権限を握る「管理者ダッシュボード」を辞書攻撃・総当たり攻撃(ブルートフォースアタック)から守るためのログイン試行制限とアカウント一時ロック機構の設計について解説します。


💡 はじめに:管理者画面という「サイト最大の急所」

第2回で解説した通り、つゔぁいちゃんねるには板の作成・削除、全スレッドのモデレーション、IP BANを一手に担う強力な 「管理者ダッシュボード(Admin Console)」 が存在します。

この管理画面は運営の要ですが、裏を返せば 「ここさえ突破されれば、サイト全体を一瞬で消滅させられる最大の急所」 でもあります。

もし攻撃者に管理者パスワードを破られた場合:

  • 全カテゴリ・全板・全スレッドの一括完全削除
  • 悪意あるフィッシングサイトやマルウェア配布サイトへの強制リダイレクトスクリプトの埋め込み
  • 利用者のIPアドレスや通報ログの流出

といった、取り返しのつかない大惨事が発生します。

そして恐ろしいことに、インターネット上のボットは、新規ドメインが開通した瞬間から /admin, /admin.html, /login といったパスを機械的にスキャンし、 辞書に載っている数万通りのパスワードを毎秒数十回の猛スピードで総当たり してきます。

この攻撃を確実に沈黙させるため、開発初期(2026年8月18日)に 「ログイン試行制限(ブルートフォース防御エンジン)」 を導入しました。


📐 防御ルールの設計:5回失敗で15分間完全遮断

一般的なWebサービスでは「パスワードが違います」とだけ返答することが多いですが、セキュリティと正当な管理者の利便性を両立するため、以下の厳密なルールを策定しました。

  1. 失敗許容回数 : 直近15分(900秒)以内に 最大5回 。
  2. 残り試行回数の可視化 : 誤ったパスワードを入力した際、「あと〇回失敗すると一時ロックされます」と警告を明示。
  3. 15分間の一時ロックアウト : 5回連続で間違えた場合、そのIPアドレスからの管理者ログインを 15分間物理遮断 (パスワードの検証処理すら行わない)。
  4. 成功時の即時リセット : 正しいパスワードでログインに成功した瞬間、過去の失敗履歴をアトミックに消去し、カウントを0に復帰。

🏗️ データベース設計:admin_login_attempts テーブル

攻撃者の試行回数とタイムスタンプを追跡するための専用テーブルを新設しました。

CREATE TABLE IF NOT EXISTS admin_login_attempts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    ip VARCHAR(45) NOT NULL,
    attempt_time INTEGER NOT NULL, -- UNIXタイムスタンプ(秒)
    success INTEGER NOT NULL DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_login_attempts_ip_time 
ON admin_login_attempts(ip, attempt_time);

スライディングウィンドウ(時間枠)による集計

固定の「0時〜24時」のような集計ではなく、 「現在時刻から過去900秒(15分間)の間に失敗が何件あるか」 を動的にカウントします。これにより、日付の変わり目などを突いた攻撃も隙間なく捉えることができます。


💻 ログイン認証エンジンの実装

public function attemptAdminLogin($inputPassword, $clientIp) {
    $now = time();
    $windowSeconds = 900; // 15分間
    $maxAttempts = 5;     // 最大5回

    // 1. 直近15分間の失敗回数をカウント
    $stmt = $this->db->prepare("
        SELECT COUNT(*) 
        FROM admin_login_attempts 
        WHERE ip = :ip 
          AND success = 0 
          AND attempt_time > :since
    ");
    $stmt->execute([
        ':ip' => $clientIp,
        ':since' => $now - $windowSeconds
    ]);
    $failedCount = (int)$stmt->fetchColumn();

    // 2. すでに5回以上失敗している場合は即座に遮断
    if ($failedCount >= $maxAttempts) {
        http_response_code(429);
        echo json_encode([
            'success' => false,
            'code' => 'LOGIN_LOCKED',
            'message' => 'ログイン失敗が上限に達したため、15分間ログインがロックされています。'
        ]);
        exit;
    }

    // 3. 正しいパスワードハッシュとの照合(タイミング攻撃対策)
    $correctHash = $this->getAdminPasswordHash();
    $isValid = password_verify($inputPassword, $correctHash);

    if ($isValid) {
        // ✅ 認証成功:過去の失敗履歴をリセット
        $stmt = $this->db->prepare("DELETE FROM admin_login_attempts WHERE ip = :ip");
        $stmt->execute([':ip' => $clientIp]);

        // セッショントークンの発行...
        return $this->generateAdminSessionToken();
    } else {
        // ❌ 認証失敗:失敗履歴を記録
        $stmt = $this->db->prepare("
            INSERT INTO admin_login_attempts (ip, attempt_time, success) 
            VALUES (:ip, :time, 0)
        ");
        $stmt->execute([':ip' => $clientIp, ':time' => $now]);

        $newFailedCount = $failedCount + 1;
        $remaining = $maxAttempts - $newFailedCount;

        http_response_code(401);
        if ($remaining > 0) {
            $msg = "パスワードが正しくありません(あと {$remaining} 回失敗すると15分間ロックされます)。";
        } else {
            $msg = "ログイン失敗が上限に達したため、15分間ログインがロックされました。";
        }

        echo json_encode([
            'success' => false,
            'code' => 'INVALID_PASSWORD',
            'remaining_attempts' => max(0, $remaining),
            'message' => $msg
        ]);
        exit;
    }
}

🔒 セキュリティをさらに強固にする「2つの隠し技」

本機能の実装にあたり、コードの細部にもセキュリティ対策を施しました。

1. タイミング攻撃(Timing Attack)の遮断

文字列の素朴な比較($a === $b)を使うと、先頭から1文字ずつ比較されるため、「1文字目が合っている時」と「1文字目から間違っている時」でレスポンス時間にナノ秒単位の差が生じます。高度なハッカーはこの微小な時間差からパスワードを推測してきます。
PHPの password_verify() や hash_equals() を用いることで、 結果に関わらず常に一定の時間で比較を完了させ、時間差による情報漏洩を物理的に封殺 しました。

2. ロック中の「DB・CPU負荷ゼロ化」

ロック状態にあるIPからのリクエストは、 重たいパスワードハッシュ計算(Argon2やbcrypt)を実行する前に最初のクエリで即座に弾く 設計にしています。これにより、攻撃者が数万回のログインリクエストを送りつけてきても、サーバーのCPUが高負荷で悲鳴を上げることはありません。


🎯 まとめと次回予告

第14回となる今回は、掲示板の最高司令塔を総当たり攻撃から守り抜く 「管理者ブルートフォース防御システム」 について解説しました。

今回の学び・ポイント

  • 急所を守る5回制限 :15分枠・5回失敗での一時ロックアウトが、辞書攻撃の試行速度を完全に奪い去る。
  • 正当な管理者への優しさ :「あと何回でロックされるか」を明示することで、管理者自身の誤入力によるロック事故を未然に防止。
  • タイミング攻撃と高負荷の二重対策 :定数時間比較と早期 exit によるサーバー資源の完全防御。

管理者認証の守りを固めました。
しかし、Webの脅威はフォームの直接攻撃だけではありません。
「ブラウザの脆弱性を突くXSS(クロスサイトスクリプティング)や、別サイトから不正に埋め込まれるクリックジャッキング攻撃」 です。

次回、 【第15回】CSP, HSTS, X-Frame-Options!XSS・クリックジャッキングを根絶するセキュリティヘッダー完全防壁 へと続きます!


🔗 関連リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?