2026年5月20日

2026年5月20日

WordPressのログイン試行回数をプラグインなしで制限する方法|コードとNginx設定

WordPressの標準機能には、ログインの失敗回数を数えて一時的に締め出す仕組みがありません。プラグインを増やさずに対策したい場合は、WordPressのフックで失敗回数を記録してロックする方法と、Webサーバー側でwp-login.phpへのアクセスを絞る方法を組み合わせます。この記事では、そのまま使えるコードと、サーバー設定の正しい書き方を説明します。

仕組み:どのフックで何をするか

WordPressのログインは wp_authenticate() 関数で処理されます。この中で authenticate フィルターが実行され、既定では優先度20の関数がユーザー名・メールアドレス・パスワードを照合します。照合の結果がエラーになると、wp_login_failed アクションが実行されます。この流れを利用して、次の3つを組み合わせます。

フック 役割
wp_login_failed 失敗したIPアドレスの回数を記録する
authenticate(優先度を既定の照合より後にする) 上限に達したIPならエラーを返してログインさせない
wp_login ログインに成功したら回数を消す

注意点が2つあります。

  • authenticate で優先度20より前にエラーを返しても、既定の照合関数がその結果を上書きします。ロックの判定は優先度20より後(この記事では100)に置きます。
  • ロックのエラーを返した場合も wp_authenticate() はそれを失敗とみなし、wp_login_failed をもう一度実行します。何もしないと、ロック中の試行まで数えてしまいます。WordPress 5.4以降は wp_login_failed の第2引数でエラーの種類を受け取れるので、自分が返したロックのエラーは数えないようにします。

なお、XML-RPC(xmlrpc.php)経由のログインも wp_authenticate() を通るため、同じ制限がかかります。

実装コード

テーマを変えても効くように、wp-content/mu-plugins/login-limit.php として置くのがおすすめです(mu-plugins フォルダーが無ければ作ります。テーマの functions.php に書いても動きます)。失敗の記録と判定は、この1つのファイルにまとめてください。記事やブログで見かけるコードを複数貼り合わせると、wp_login_failed に同じ処理が重なり、1回の失敗が2回以上数えられます。

<?php
/**
 * Plugin Name: Login Limit (simple)
 * Description: IPアドレスごとにログイン失敗回数を数え、上限に達したら一定時間ロックする。
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

const WPDR_LOGIN_MAX_FAILS   = 5;   // この回数失敗したらロック
const WPDR_LOGIN_LOCK_SECOND = 900; // ロック時間(秒)。最後の失敗から数える

function wpdr_login_client_ip(): string {
	// リバースプロキシやCDNの内側では REMOTE_ADDR がプロキシのIPになる(後述)
	return isset( $_SERVER['REMOTE_ADDR'] ) ? (string) $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
}

function wpdr_login_key( string $ip ): string {
	return 'wpdr_login_fail_' . md5( $ip );
}

// 1) 上限に達したIPはログインさせない(既定の照合より後に実行)
add_filter(
	'authenticate',
	function ( $user, $username, $password ) {
		if ( '' === (string) $username && '' === (string) $password ) {
			return $user; // ログイン画面を開いただけのとき
		}
		$fails = (int) get_transient( wpdr_login_key( wpdr_login_client_ip() ) );
		if ( $fails >= WPDR_LOGIN_MAX_FAILS ) {
			return new WP_Error(
				'wpdr_login_locked',
				sprintf(
					'<strong>エラー:</strong> ログインの失敗が続いたため、一時的にログインできません。%d分ほど待ってからお試しください。',
					(int) ceil( WPDR_LOGIN_LOCK_SECOND / 60 )
				)
			);
		}
		return $user;
	},
	100,
	3
);

// 2) 失敗を数える(ロックのエラーは数えない)
add_action(
	'wp_login_failed',
	function ( $username, $error = null ) {
		if ( $error instanceof WP_Error && 'wpdr_login_locked' === $error->get_error_code() ) {
			return;
		}
		$ip    = wpdr_login_client_ip();
		$key   = wpdr_login_key( $ip );
		$fails = (int) get_transient( $key ) + 1;
		set_transient( $key, $fails, WPDR_LOGIN_LOCK_SECOND );

		if ( WPDR_LOGIN_MAX_FAILS === $fails ) {
			error_log( sprintf( '[login-limit] locked ip=%s user=%s', $ip, $username ) );
			// 管理者への通知。ロックが始まったときに1回だけ送る
			wp_mail(
				get_option( 'admin_email' ),
				sprintf( '[%s] ログインの失敗が続いたIPをロックしました', wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ),
				sprintf( "IPアドレス: %s\nユーザー名: %s\n日時: %s", $ip, $username, current_time( 'mysql' ) )
			);
		}
	},
	10,
	2
);

// 3) ログインに成功したら回数を消す
add_action(
	'wp_login',
	function () {
		delete_transient( wpdr_login_key( wpdr_login_client_ip() ) );
	}
);

このコードの動きは次のとおりです。

  • 同じIPから5回失敗すると、その後のログインはロックのエラーになります。
  • 回数は transient に保存し、失敗するたびに有効期限を15分に延ばします。最後の失敗から15分たつと回数が消え、ロックも解けます。
  • ロック中は、正しいパスワードを入れてもログインできません。
  • 通知メールはロックが始まった1回だけ送ります。攻撃が多いサイトではメールが増えるので、不要なら wp_mail() の部分を消してください。

動作を確かめる

自分のIPがロックされても困らないように、別のブラウザーかシークレットウィンドウで試します。

  1. わざと違うパスワードで5回ログインし、6回目にロックのメッセージが出ることを確認します。
  2. 正しいパスワードを入れてもロックのメッセージになることを確認します。
  3. 次の手順でロックを解除し、ログインできることを確認します。

自分がロックされたときの解除方法

WP-CLIが使える場合は、対象のIPアドレスを指定して transient を消します(203.0.113.5 は自分のIPに置き換えます)。

wp eval "delete_transient( 'wpdr_login_fail_' . md5( '203.0.113.5' ) );"

WP-CLIが使えない場合は、FTPやファイルマネージャーで wp-content/mu-plugins/login-limit.php の名前を一時的に変えるとロックが外れます。ログインできたら名前を戻してください。

コードだけで防げないこと

  • 共有IPの巻き添え:会社や学校のように多くの人が同じIPアドレスから接続する環境では、他人の失敗で自分もロックされます。上限回数とロック時間は、利用者の環境に合わせて調整してください。
  • プロキシ・CDNの内側:Cloudflareなどのリバースプロキシを通している場合、REMOTE_ADDR はプロキシのIPになり、全員が同じIPとして数えられます。Webサーバー側で本来の接続元IPを復元する設定(Nginxの real_ip モジュール、Apacheの mod_remoteip)をしてから使ってください。X-Forwarded-For ヘッダーをそのまま信用すると、攻撃者が値を偽装できます。
  • 分散した攻撃:多数のIPから少しずつ試す攻撃には、IP単位の回数制限は効きにくくなります。強いパスワードと二要素認証を併用してください。
  • サーバーの負荷:ロック中でも、パスワードの照合(優先度20の処理)は実行されます。大量のリクエストで負荷が高い場合は、次のサーバー側の制限を組み合わせます。
  • 保存先:永続オブジェクトキャッシュ(RedisやMemcachedのドロップイン)が無い環境では、transient はデータベースの options テーブルに保存されます。オブジェクトキャッシュがある環境ではキャッシュに保存され、キャッシュの消去や追い出しで回数が消えることがあります。

サーバー側でwp-login.phpへのアクセスを絞る

Nginx:limit_req でリクエスト数を制限する

Nginxでは limit_req_zone と limit_req で、IPアドレスごとのリクエスト数を制限できます。limit_req_zone は http ブロックに、limit_req は location に書きます。

# http ブロック(例: /etc/nginx/conf.d/limit-login.conf)
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=10r/m;

# server ブロック内
location = /wp-login.php {
    limit_req zone=wplogin burst=5 nodelay;
    limit_req_status 429;

    # = の完全一致の location は、PHP用の正規表現 location より優先される。
    # そのため、ここにもPHPを実行する設定を書く(既存の PHP の location と同じ内容にする)
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php-fpm.sock;
}

この例では、1つのIPから1分あたり10回程度を超えるアクセスに429を返します。burst は一時的な超過をどこまで受け付けるかの値です。fastcgi_pass のソケットのパスは、既存のPHPの設定に合わせてください。設定後は sudo nginx -t で構文を確認してから sudo systemctl reload nginx で反映します。

Apache:.htaccessでアクセス元を限定する

Apacheには、.htaccessで使える「リクエスト数」の制限機能が標準ではありません。よく紹介される mod_ratelimit は転送速度(帯域)を絞るモジュールで、ログインの試行回数は減らせません。また <Location> は .htaccess では使えず、書くと500エラーになります。

.htaccessでできる現実的な対策は、wp-login.php に入れる人を限定することです。管理者の接続元IPアドレスが決まっている場合は、Apache 2.4の構文で次のように書きます(サーバー側で AllowOverride AuthConfig が許可されている必要があります)。

# WordPress ルートの .htaccess(# BEGIN WordPress より前)
<Files "wp-login.php">
    Require ip 203.0.113.5 198.51.100.0/24
</Files>

# XML-RPC を使わない場合は遮断する
<Files "xmlrpc.php">
    Require all denied
</Files>

Order Deny,Allow や Deny from all はApache 2.2の構文で、2.4では互換モジュール(mod_access_compat)が無いと動きません。2.4では Require を使ってください。xmlrpc.php を遮断すると、Jetpackや一部のスマートフォンアプリなど XML-RPC を使う機能が動かなくなるので、使っていないことを確認してから設定します。

IPアドレスが固定できない場合は、wp-login.php にBasic認証をかける方法もあります。いずれも設定後に、wp-login.php とサイトのトップページの両方が想定どおりに表示されるかを確認してください。

まとめ

  • 失敗の記録は wp_login_failed、ロックの判定は authenticate(優先度20より後)、解除は wp_login で行う
  • コードは1ファイルにまとめ、同じフックに処理を重ねない。ロックのエラーは wp_login_failed の第2引数で見分けて数えない
  • プロキシの内側では本来の接続元IPを復元してから使う。共有IPの巻き添えに注意する
  • Nginxは limit_req でwp-login.phpへのリクエスト数を制限できる。mod_ratelimit は帯域制限なので使わない
  • Apacheの .htaccess では <Location> は使えない。<Files> と Require でアクセス元を限定する

関連記事:

お気軽にご相談ください

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