WordPressでプラグインを使わずに管理画面の不正アクセス対策を行う

WordPressのプラグインは便利なのですが、最近はちょっと“邪悪”なものが増えてきた気がします。アップデートで別物になったり、急に課金前提になったり…。

というわけで、プラグインを削除してアクセス制限の処理を自作してみました。


管理画面の不正ログインを制限

functions.php」に追記して実行します。

<?php

///=======================================================================
//   *管理画面の不正ログインを制限
///=======================================================================

define('IB_MAX_LOGIN_ATTEMPTS', 3);   // 何回連続で失敗したらロックするか
define('IB_LOCKOUT_MINUTES', 60);     // ロック時間(分単位)

//---------------------------------------------
//   *クライアントIPを取得
//---------------------------------------------

function ib_get_client_ip(){

	foreach(['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'] as $key){

		if(!empty($_SERVER[$key])){

			//---カンマ区切りの場合は先頭だけ使う
			$ip = explode(',', $_SERVER[$key])[0];

			return trim($ip);
		}
	}

	return 'unknown';
}

//---------------------------------------------
//   *ログイン失敗時にカウントアップ
//---------------------------------------------

function ib_wp_login_failed($username){

	$ip = ib_get_client_ip();
	$state = ib_get_login_state($ip);

	if($state['locked_until'] > time()){
		return;
	}

	$state['count']++;
	$state['last_username'] = $username;

	if($state['count'] >= IB_MAX_LOGIN_ATTEMPTS){
		$state['locked_until'] = time() + (IB_LOCKOUT_MINUTES * 60);
	}

	ib_set_login_state($ip, $state);
}

add_action("wp_login_failed", "ib_wp_login_failed");

//---------------------------------------------
//   *IPごとの失敗状態を取得
//---------------------------------------------

function ib_get_login_state($ip){

	$key = 'ib_login_limit_' . md5($ip);
	$state = get_transient($key);

	if(!is_array($state)){
		$state = [
			'ip' => $ip,
			'count' => 0,
			'locked_until' => 0,
			'updated_at' => 0,
			'last_username' => '',
			'logged_in' => 0,
		];
	}

	return $state;
}

//---------------------------------------------
//   *IPごとの失敗状態を保存
//---------------------------------------------

function ib_set_login_state($ip, $state){

	$key = 'ib_login_limit_' . md5($ip);

	$state['ip'] = $ip;
	$state['updated_at'] = time();

	//---ロック時間 + 余裕分だけ保持(ここでは24時間)
	$expiration = (IB_LOCKOUT_MINUTES * 60) + 86400;

	set_transient($key, $state, $expiration);
}

//---------------------------------------------
//   *認証成功時にカウンタをリセット
//---------------------------------------------

function ib_authenticate($user, $username, $password){

	$ip = ib_get_client_ip();
	$state = ib_get_login_state($ip);

	//---ログインに成功している
	if($user instanceof WP_User){
		$state['count'] = 0;
		$state['locked_until'] = 0;
		$state['last_username'] = $username;
		$state['logged_in'] = 1;

		ib_set_login_state($ip, $state);
	}

	return $user;
}

add_filter("authenticate", "ib_authenticate", 30, 3);

//---------------------------------------------
//   *ロック時にログインフォームで403
//---------------------------------------------

function ib_block_locked_ip_before_form(){

	$ip = ib_get_client_ip();
	$state = ib_get_login_state($ip);

	if($state['locked_until'] <= time()){
		return;
	}

	$remaining = $state['locked_until'] - time();
	$min = ceil($remaining / 60);

	if($min < 1){
		$min = 1;
	}

	wp_die(
		"このIPアドレス({$ip})からのログイン試行回数が多すぎます。<br>あと {$min} 分後に再度お試しください。",
		'ログイン制限中',
		[
			'response' => 403,
			'back_link' => false,
		]
	);
}

add_action('login_init', 'ib_block_locked_ip_before_form');



アクセス制限の内容

過去24時間以内に3回以上アクセスに失敗したIPアドレスに対しては、60分間「403 エラー」を返してアクセスを制限しています。

403 エラーを返すことで、お行儀の良いBOTはこちらへのアクセスを諦めてくれます。

実際、単にコメント欄に「エラーです」と表示してもBOTのアクセスは止まりませんでしたが、403 エラーを返すようにしたところアクセスが止まりました(攻撃側にとっても無駄な時間だと判断されるためです)。

意外と簡単だったので、今後もプラグインを自作して置き換えていきたいと思います!