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フックでロックアウト解除機能を提供します。