2026年5月29日

2026年5月29日

WordPressのログイン試行回数を制限してブルートフォース攻撃を防ぐ方法

はじめに

WordPressのwp-login.phpはデフォルトで無制限のログイン試行を許可しています。これを悪用したブルートフォース攻撃(パスワード総当たり)はWordPressへの最も一般的な攻撃手法のひとつです。ログイン試行を制限することで大幅に防御力を高められます。

症状・原因

  • アクセスログにPOST /wp-login.phpが毎秒数件記録されている
  • 管理者パスワードが変更されていて管理画面にログインできない
  • サーバーのCPU負荷が異常に高くなっている
  • 見知らぬIPアドレスからの大量アクセスがある

解決手順

ステップ1:コードでログイン試行回数を制限する

// functions.php: ログイン試行回数制限
class Login_Attempt_Limiter {
    private const MAX_ATTEMPTS  = 5;
    private const LOCKOUT_TIME  = 15 * MINUTE_IN_SECONDS;
    private const LOG_GROUP     = 'login_security';

    public function __construct() {
        add_filter('authenticate',           [$this, 'check_attempts'], 30, 3);
        add_action('wp_login_failed',        [$this, 'record_failed_attempt']);
        add_filter('login_errors',           [$this, 'generic_error_message']);
        add_action('wp_login',               [$this, 'clear_attempts'], 10, 2);
    }

    public function check_attempts(WP_User|WP_Error|null $user, string $username, string $password): mixed {
        $ip     = $this->get_client_ip();
        $locked = get_transient('login_locked_' . md5($ip));

        if ($locked) {
            $remaining = human_time_diff(time(), $locked);
            return new WP_Error(
                'too_many_retries',
                sprintf('ログイン試行回数が多すぎます。%s後に再試行してください。', $remaining)
            );
        }

        return $user;
    }

    public function record_failed_attempt(string $username): void {
        $ip         = $this->get_client_ip();
        $key        = 'login_attempts_' . md5($ip);
        $attempts   = (int) get_transient($key) + 1;

        set_transient($key, $attempts, self::LOCKOUT_TIME);

        if ($attempts >= self::MAX_ATTEMPTS) {
            $unlock_time = time() + self::LOCKOUT_TIME;
            set_transient('login_locked_' . md5($ip), $unlock_time, self::LOCKOUT_TIME);

            // ログに記録
            error_log(sprintf(
                '[Login Lock] IP: %s, Username: %s, Attempts: %d',
                $ip, $username, $attempts
            ));
        }
    }

    public function clear_attempts(string $user_login, WP_User $user): void {
        $ip = $this->get_client_ip();
        delete_transient('login_attempts_' . md5($ip));
        delete_transient('login_locked_' . md5($ip));
    }

    // エラーメッセージを汎用化(ユーザー名/パスワードのどちらが間違いか分からなくする)
    public function generic_error_message(): string {
        return 'ユーザー名またはパスワードが正しくありません。';
    }

    private function get_client_ip(): string {
        return sanitize_text_field(
            $_SERVER['HTTP_CF_CONNECTING_IP']
            ?? $_SERVER['HTTP_X_FORWARDED_FOR']
            ?? $_SERVER['REMOTE_ADDR']
            ?? 'unknown'
        );
    }
}

new Login_Attempt_Limiter();

ステップ2:reCAPTCHA v3をログインページに追加する

// Google reCAPTCHA v3 をログインページに組み込む
class WP_Login_Recaptcha {
    private const SITE_KEY   = 'YOUR_SITE_KEY';
    private const SECRET_KEY = 'YOUR_SECRET_KEY';
    private const MIN_SCORE  = 0.5;

    public function __construct() {
        add_action('login_enqueue_scripts', [$this, 'enqueue_script']);
        add_action('login_form',            [$this, 'add_hidden_field']);
        add_filter('authenticate',          [$this, 'verify_token'], 20, 3);
    }

    public function enqueue_script(): void {
        wp_enqueue_script(
            'google-recaptcha',
            'https://www.google.com/recaptcha/api.js?render=' . self::SITE_KEY,
            [],
            null,
            true
        );

        wp_add_inline_script('google-recaptcha', sprintf('
            grecaptcha.ready(function() {
                grecaptcha.execute("%s", {action: "login"}).then(function(token) {
                    document.getElementById("g-recaptcha-response").value = token;
                });
            });
        ', self::SITE_KEY));
    }

    public function add_hidden_field(): void {
        echo '<input type="hidden" id="g-recaptcha-response" name="g-recaptcha-response" value="">';
    }

    public function verify_token(WP_User|WP_Error|null $user, string $username, string $password): mixed {
        if (empty($username)) return $user;

        $token = sanitize_text_field($_POST['g-recaptcha-response'] ?? '');

        if (!$token) {
            return new WP_Error('recaptcha_failed', 'reCAPTCHA の確認に失敗しました。もう一度お試しください。');
        }

        $response = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', [
            'body' => [
                'secret'   => self::SECRET_KEY,
                'response' => $token,
                'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '',
            ],
        ]);

        if (is_wp_error($response)) return $user;

        $result = json_decode(wp_remote_retrieve_body($response), true);

        if (!($result['success'] ?? false) || ($result['score'] ?? 0) < self::MIN_SCORE) {
            return new WP_Error('recaptcha_failed', 'reCAPTCHA スコアが低すぎます。ブラウザを更新してお試しください。');
        }

        return $user;
    }
}

new WP_Login_Recaptcha();

ステップ3:wp-login.phpを特定IPのみに制限する

# .htaccess: wp-login.phpのアクセスをIP制限
<Files "wp-login.php">
    Order Deny,Allow
    Deny from all
    Allow from 203.0.113.1    # 自宅IP
    Allow from 198.51.100.0   # 会社IP
</Files>
# Nginx: wp-login.phpをIP制限
location = /wp-login.php {
    allow 203.0.113.1;
    allow 198.51.100.0/24;
    deny all;

    # 許可されたIPでもレート制限
    limit_req zone=wp_login burst=3 nodelay;

    fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

ステップ4:ログインURLを変更する

// wp-login.phpのURLを変更する(セキュリティ by obscurity)
// WP Hide Login プラグインの動作を手動で実装
add_action('init', function(): void {
    $new_login = 'my-secret-login'; // 変更後のログインURL

    // カスタムログインURLをルーティング
    if ($_SERVER['REQUEST_URI'] === "/{$new_login}") {
        $_SERVER['REQUEST_URI'] = '/wp-login.php';
        require ABSPATH . 'wp-login.php';
        exit;
    }

    // 元のwp-login.phpへのアクセスをブロック
    if (str_ends_with($_SERVER['REQUEST_URI'], 'wp-login.php')
        && !is_admin()) {
        wp_redirect(home_url('/'), 301);
        exit;
    }
});

ステップ5:ログインアクティビティを監視する

// ログイン/ログアウトを記録
add_action('wp_login', function(string $user_login, WP_User $user): void {
    error_log(sprintf(
        '[Login] User: %s | IP: %s | Time: %s',
        $user_login,
        sanitize_text_field($_SERVER['REMOTE_ADDR'] ?? 'unknown'),
        current_time('Y-m-d H:i:s')
    ));
}, 10, 2);

add_action('wp_login_failed', function(string $username): void {
    error_log(sprintf(
        '[Login Failed] Username: %s | IP: %s | Time: %s',
        $username,
        sanitize_text_field($_SERVER['REMOTE_ADDR'] ?? 'unknown'),
        current_time('Y-m-d H:i:s')
    ));
});

// ロックされたIPリストを管理画面に表示
add_action('admin_notices', function(): void {
    if (!current_user_can('manage_options')) return;

    global $wpdb;
    $locked = $wpdb->get_col("
        SELECT option_name FROM {$wpdb->options}
        WHERE option_name LIKE '_transient_login_locked_%'
    ");

    if ($locked) {
        echo '<div class="notice notice-warning"><p>';
        echo count($locked) . ' 件のIPがロックされています。';
        echo '</p></div>';
    }
});

注意事項

  • 自分のIPを必ず確認: IP制限を設定する前に自分のIPアドレスを確認し、ロックアウトされないようにしてください
  • 動的IP: 動的IPアドレスを使用している場合はIP制限より試行回数制限の方が適切です
  • 二要素認証: ログイン試行制限に加えて二要素認証(2FA)を組み合わせると防御力が大幅に向上します
  • reCAPTCHAのキー: Google reCAPTCHA v3のサイトキー・シークレットキーはGoogle reCAPTCHAの管理コンソールで取得してください

まとめ

ログイン保護の最適な組み合わせは「試行回数制限(5回でロック)・reCAPTCHA v3・Nginxレート制限・ログインURL変更」の4層です。これらを組み合わせることでブルートフォース攻撃をほぼ完全にブロックできます。関連記事:WordPressのWAF設定方法

お気軽にご相談ください

お見積りへ お問い合わせへ