2026年5月20日

2026年5月20日

WordPressのログイン試行をプラグインなしで制限する方法

はじめに

プラグインを増やしたくない、または軽量な実装でログイン保護をしたいケースで役立つ、プラグイン不要のログイン試行制限の実装方法です。WordPressのフックとサーバー設定だけで効果的な防御を構築できます。

症状・原因

  • セキュリティプラグインを入れたくないが、ブルートフォース対策は必要
  • Wordfenceなどの重いプラグインがサイト速度に影響している
  • シンプルなコードで試行回数制限を自前実装したい
  • .htaccessまたはNginxでサーバーレベルの制限をかけたい

解決手順

ステップ1:transientを使ったログイン試行制限

// functions.php: プラグインなしのログイン試行制限
// 設定値
define('LOGIN_MAX_ATTEMPTS', 5);       // 最大試行回数
define('LOGIN_LOCKOUT_DURATION', 900); // ロック時間(秒)= 15分

/**
 * ログイン試行回数を取得するトランジェントキーを生成
 */
function get_login_attempt_key(string $ip): string {
    return 'login_fail_' . md5($ip);
}

/**
 * ロックアウト中かどうかを確認
 */
function is_login_locked_out(string $ip): bool {
    $attempts = (int) get_transient(get_login_attempt_key($ip));
    return $attempts >= LOGIN_MAX_ATTEMPTS;
}

// ログイン前にロックアウト確認
add_filter('authenticate', function($user, string $username, string $password) {
    if (empty($username) && empty($password)) return $user;

    $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';

    if (is_login_locked_out($ip)) {
        $remaining = get_transient(get_login_attempt_key($ip) . '_time');
        return new WP_Error(
            'login_locked',
            sprintf(
                '<strong>エラー</strong>: ログイン試行回数の上限に達しました。' .
                '%d分後に再試行できます。',
                (int) (LOGIN_LOCKOUT_DURATION / 60)
            )
        );
    }

    return $user;
}, 30, 3);

// ログイン失敗時にカウントアップ
add_action('wp_login_failed', function(string $username): void {
    $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
    $key = get_login_attempt_key($ip);
    $attempts = (int) get_transient($key);

    // 失敗を記録
    set_transient($key, $attempts + 1, LOGIN_LOCKOUT_DURATION);

    // 失敗ログを記録(オプション)
    error_log(sprintf(
        '[WordPress] Login failed: user=%s ip=%s attempts=%d',
        $username, $ip, $attempts + 1
    ));
});

// ログイン成功時にカウントをリセット
add_action('wp_login', function(string $user_login): void {
    $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
    delete_transient(get_login_attempt_key($ip));
});

ステップ2:ユーザー名とIPの両方で追跡する

// functions.php: IPとユーザー名の組み合わせで追跡(より精密)
add_action('wp_login_failed', function(string $username): void {
    $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';

    // IPベースの記録
    $ip_key = 'lf_ip_' . md5($ip);
    $ip_count = (int) get_transient($ip_key);
    set_transient($ip_key, $ip_count + 1, 900);

    // ユーザー名ベースの記録(辞書攻撃対策)
    if (!empty($username)) {
        $user_key = 'lf_user_' . md5($username);
        $user_count = (int) get_transient($user_key);
        set_transient($user_key, $user_count + 1, 900);
    }
});

add_filter('authenticate', function($user, string $username, string $password) {
    if (empty($username) && empty($password)) return $user;

    $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
    $ip_attempts = (int) get_transient('lf_ip_' . md5($ip));
    $user_attempts = !empty($username)
        ? (int) get_transient('lf_user_' . md5($username))
        : 0;

    if ($ip_attempts >= 5 || $user_attempts >= 10) {
        return new WP_Error(
            'too_many_login_attempts',
            '<strong>エラー</strong>: ログイン試行が多すぎます。しばらくお待ちください。'
        );
    }

    return $user;
}, 30, 3);

ステップ3:管理者に通知する

// functions.php: ブルートフォース攻撃を検出したら管理者にメール
add_action('wp_login_failed', function(string $username): void {
    $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
    $key = 'lf_ip_' . md5($ip);
    $attempts = (int) get_transient($key);

    // 5回失敗時に1回だけ通知
    if ($attempts === 4) { // 0-indexed なので4=5回目
        $admin_email = get_option('admin_email');
        $site_name = get_bloginfo('name');
        $time = current_time('Y-m-d H:i:s');

        wp_mail(
            $admin_email,
            "[{$site_name}] ブルートフォース攻撃を検出しました",
            "IPアドレス {$ip} からのログイン試行が5回に達しました。\n" .
            "対象ユーザー名: {$username}\n" .
            "日時: {$time}\n" .
            "15分間アクセスをブロックしています。"
        );
    }
});

ステップ4:.htaccessでサーバーレベルの制限を追加する

# .htaccess: wp-login.php へのPOSTリクエストをレートリミット
# ※ mod_ratelimit が必要(Apache 2.4以降)
<IfModule mod_ratelimit.c>
<Location /wp-login.php>
    SetOutputFilter RATE_LIMIT
    SetEnv rate-limit 400
</Location>
</IfModule>

# XMLRPCを完全にブロック(ブルートフォースに悪用される)
<Files xmlrpc.php>
    Order Deny,Allow
    Deny from all
</Files>

# wp-login.php への過度なアクセスをブロック
# (Fail2ban と組み合わせる場合はコメントアウト)
<IfModule mod_rewrite.c>
RewriteEngine On
# User-Agent が空のリクエストをブロック
RewriteCond %{HTTP_USER_AGENT} ^$
RewriteRule ^wp-login\.php$ - [F,L]
</IfModule>

ステップ5:ロックアウト状態を管理する

// functions.php: 管理者がロックアウト状態をリセットできる機能
add_action('admin_post_reset_login_lockout', function(): void {
    if (!current_user_can('manage_options')) {
        wp_die('権限がありません');
    }

    check_admin_referer('reset_login_lockout');

    $ip = sanitize_text_field($_POST['blocked_ip'] ?? '');
    if ($ip) {
        delete_transient('lf_ip_' . md5($ip));
        delete_transient('login_fail_' . md5($ip));
    }

    wp_redirect(add_query_arg(
        ['page' => 'login-security', 'reset' => '1'],
        admin_url('options-general.php')
    ));
    exit;
});

// 現在のロックアウト状況を確認するWP-CLIコマンド
// wp eval "global \$wpdb; \$keys = \$wpdb->get_col(\"SELECT option_name FROM {\$wpdb->options} WHERE option_name LIKE '_transient_lf_ip_%'\"); foreach(\$keys as \$k) { \$ip_hash = str_replace('_transient_lf_ip_', '', \$k); echo \$k . ': ' . get_transient('lf_ip_' . \$ip_hash) . PHP_EOL; }"

注意事項

  • トランジェントベースの実装はオブジェクトキャッシュ(Redis/Memcached)が有効な場合に正しく動作しますが、ページキャッシュのフラッシュではリセットされません
  • 共有IPアドレス(企業のNAT環境等)から複数ユーザーがログインする場合、無実のユーザーがロックアウトされる可能性があります。ユーザー名ベースの制限はこのリスクを軽減します
  • 自分自身がロックアウトされた場合は、WP-CLIでwp eval "delete_transient('lf_ip_' . md5('あなたのIP'));"を実行してリセットできます

まとめ

プラグインなしのログイン試行制限は①authenticateフィルターで試行前にロックアウト確認、②wp_login_failedアクションでトランジェントにIP単位の失敗回数を記録(15分間有効)、③5回失敗時に管理者メール通知、④.htaccessでUser-Agentが空のリクエストとXMLRPCをブロック、⑤WP-CLIやadmin_postフックでロックアウト解除機能を提供します。

お気軽にご相談ください

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