I’ll build this as a standalone inc/security/login-security.php module for your RX theme, with safe defaults, WordPress hooks, login hardening, rate limiting, bot checks, headers, XML-RPC controls, REST restrictions, audit logging, and admin settings constants.
Below is an advanced inc/security/login-security.php file for your RX theme. It adds many safe login/security functions without needing a plugin.
Add this file here:
rx-theme/
└── inc/
└── security/
└── login-security.php
Then load it in your functions.php:
require_once get_template_directory() . '/inc/security/login-security.php';
Now create this file:
<?php
/**
* RX Theme - Login Security Module
*
* Path: inc/security/login-security.php
*
* Features:
* - Hide detailed login errors
* - Limit failed login attempts
* - Temporary IP + username lockout
* - Login honeypot field
* - Bot user-agent blocking
* - XML-RPC hardening
* - REST API user endpoint protection
* - Author enumeration blocking
* - Disable file editor
* - Remove WordPress version exposure
* - Security headers
* - Login page noindex
* - Username protection
* - Registration spam protection
* - Stronger password validation
* - Application password control
* - Login audit hooks
* - Optional admin email alert after lockout
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'RX_Login_Security' ) ) :
final class RX_Login_Security {
/**
* Version.
*/
const VERSION = '1.0.0';
/**
* Transient prefix.
*/
const PREFIX = 'rx_login_sec_';
/**
* Default max failed attempts.
*/
const DEFAULT_MAX_ATTEMPTS = 5;
/**
* Default lockout time in seconds.
*/
const DEFAULT_LOCKOUT_SECONDS = 1800; // 30 minutes.
/**
* Constructor.
*/
private function __construct() {
$this->define_constants();
$this->hooks();
}
/**
* Init singleton.
*/
public static function init() {
static $instance = null;
if ( null === $instance ) {
$instance = new self();
}
return $instance;
}
/**
* Define safe constants if not already defined.
*/
private function define_constants() {
$this->maybe_define( 'RX_LOGIN_SECURITY_MAX_ATTEMPTS', self::DEFAULT_MAX_ATTEMPTS );
$this->maybe_define( 'RX_LOGIN_SECURITY_LOCKOUT_SECONDS', self::DEFAULT_LOCKOUT_SECONDS );
$this->maybe_define( 'RX_LOGIN_SECURITY_BLOCK_XMLRPC', true );
$this->maybe_define( 'RX_LOGIN_SECURITY_DISABLE_APP_PASSWORDS', false );
$this->maybe_define( 'RX_LOGIN_SECURITY_BLOCK_AUTHOR_ENUM', true );
$this->maybe_define( 'RX_LOGIN_SECURITY_HIDE_WP_VERSION', true );
$this->maybe_define( 'RX_LOGIN_SECURITY_SEND_LOCKOUT_EMAIL', false );
$this->maybe_define( 'RX_LOGIN_SECURITY_STRONG_PASSWORDS', true );
$this->maybe_define( 'RX_LOGIN_SECURITY_BLOCK_REST_USERS_PUBLIC', true );
$this->maybe_define( 'RX_LOGIN_SECURITY_ENABLE_HEADERS', true );
$this->maybe_define( 'RX_LOGIN_SECURITY_LOG_EVENTS', false );
}
/**
* Define constant safely.
*/
private function maybe_define( $name, $value ) {
if ( ! defined( $name ) ) {
define( $name, $value );
}
}
/**
* Register hooks.
*/
private function hooks() {
add_filter( 'login_errors', array( $this, 'generic_login_error' ) );
add_filter( 'authenticate', array( $this, 'check_login_lockout_before_auth' ), 1, 3 );
add_action( 'wp_login_failed', array( $this, 'handle_failed_login' ) );
add_action( 'wp_login', array( $this, 'handle_successful_login' ), 10, 2 );
add_action( 'login_form', array( $this, 'add_login_honeypot' ) );
add_filter( 'authenticate', array( $this, 'check_login_honeypot' ), 5, 3 );
add_action( 'login_init', array( $this, 'block_bad_login_requests' ) );
add_action( 'login_head', array( $this, 'login_page_noindex' ) );
add_filter( 'xmlrpc_enabled', array( $this, 'maybe_disable_xmlrpc' ) );
add_filter( 'xmlrpc_methods', array( $this, 'remove_xmlrpc_methods' ) );
add_action( 'init', array( $this, 'block_author_enumeration' ), 1 );
add_filter( 'rest_endpoints', array( $this, 'protect_rest_user_endpoints' ) );
add_action( 'send_headers', array( $this, 'send_security_headers' ) );
add_filter( 'the_generator', '__return_empty_string' );
add_action( 'init', array( $this, 'remove_wp_version_exposure' ) );
add_filter( 'wp_is_application_passwords_available', array( $this, 'maybe_disable_application_passwords' ) );
add_action( 'user_profile_update_errors', array( $this, 'validate_profile_password_strength' ), 10, 3 );
add_filter( 'registration_errors', array( $this, 'validate_registration_security' ), 10, 3 );
add_action( 'init', array( $this, 'security_runtime_defines' ), 0 );
}
/**
* Runtime WordPress security defines.
*/
public function security_runtime_defines() {
if ( ! defined( 'DISALLOW_FILE_EDIT' ) ) {
define( 'DISALLOW_FILE_EDIT', true );
}
}
/**
* Generic login error.
*/
public function generic_login_error() {
return esc_html__( 'Login failed. Please check your details and try again.', 'rx-theme' );
}
/**
* Get real-ish visitor IP.
*/
private function get_ip() {
$ip_keys = array(
'HTTP_CF_CONNECTING_IP',
'HTTP_X_REAL_IP',
'HTTP_X_FORWARDED_FOR',
'REMOTE_ADDR',
);
foreach ( $ip_keys as $key ) {
if ( empty( $_SERVER[ $key ] ) ) {
continue;
}
$value = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
if ( 'HTTP_X_FORWARDED_FOR' === $key ) {
$parts = explode( ',', $value );
$value = trim( $parts[0] );
}
if ( filter_var( $value, FILTER_VALIDATE_IP ) ) {
return $value;
}
}
return '0.0.0.0';
}
/**
* Get user agent.
*/
private function get_user_agent() {
if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
return '';
}
return substr(
sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ),
0,
255
);
}
/**
* Get login identity.
*/
private function get_identity_key( $username = '' ) {
$ip = $this->get_ip();
$username = is_string( $username ) ? strtolower( sanitize_user( $username ) ) : '';
$raw = $ip . '|' . $username;
return self::PREFIX . 'identity_' . md5( $raw );
}
/**
* Get IP-only key.
*/
private function get_ip_key() {
return self::PREFIX . 'ip_' . md5( $this->get_ip() );
}
/**
* Get transient data.
*/
private function get_attempt_data( $username = '' ) {
$key = $this->get_identity_key( $username );
$data = get_transient( $key );
if ( ! is_array( $data ) ) {
$data = array(
'attempts' => 0,
'locked' => false,
'last_time' => 0,
);
}
return $data;
}
/**
* Save attempt data.
*/
private function save_attempt_data( $username, $data, $ttl = null ) {
$key = $this->get_identity_key( $username );
if ( null === $ttl ) {
$ttl = absint( RX_LOGIN_SECURITY_LOCKOUT_SECONDS );
}
set_transient( $key, $data, $ttl );
}
/**
* Delete attempt data after success.
*/
private function clear_attempt_data( $username ) {
delete_transient( $this->get_identity_key( $username ) );
delete_transient( $this->get_ip_key() );
}
/**
* Check lockout before WordPress authenticates.
*/
public function check_login_lockout_before_auth( $user, $username, $password ) {
if ( empty( $username ) ) {
return $user;
}
$data = $this->get_attempt_data( $username );
if ( ! empty( $data['locked'] ) ) {
$remaining = $this->get_lockout_remaining_time( $username );
return new WP_Error(
'rx_login_locked',
sprintf(
esc_html__( 'Too many failed login attempts. Please try again later.', 'rx-theme' ),
absint( $remaining )
)
);
}
return $user;
}
/**
* Get lockout remaining time.
*/
private function get_lockout_remaining_time( $username ) {
$data = $this->get_attempt_data( $username );
if ( empty( $data['locked_until'] ) ) {
return 0;
}
return max( 0, absint( $data['locked_until'] ) - time() );
}
/**
* Handle failed login.
*/
public function handle_failed_login( $username ) {
$username = sanitize_user( $username );
$data = $this->get_attempt_data( $username );
$data['attempts'] = absint( $data['attempts'] ) + 1;
$data['last_time'] = time();
$data['ip'] = $this->get_ip();
$data['ua'] = $this->get_user_agent();
$max_attempts = absint( RX_LOGIN_SECURITY_MAX_ATTEMPTS );
if ( $data['attempts'] >= $max_attempts ) {
$data['locked'] = true;
$data['locked_until'] = time() + absint( RX_LOGIN_SECURITY_LOCKOUT_SECONDS );
$this->maybe_send_lockout_email( $username, $data );
$this->log_event( 'lockout', $username, $data );
} else {
$this->log_event( 'failed_login', $username, $data );
}
$this->save_attempt_data( $username, $data );
}
/**
* Handle successful login.
*/
public function handle_successful_login( $user_login, $user ) {
$this->clear_attempt_data( $user_login );
$this->log_event(
'successful_login',
$user_login,
array(
'user_id' => isset( $user->ID ) ? absint( $user->ID ) : 0,
'ip' => $this->get_ip(),
'ua' => $this->get_user_agent(),
'time' => time(),
)
);
}
/**
* Add honeypot field to login form.
*/
public function add_login_honeypot() {
?>
<p style="display:none !important;">
<label for="rx_login_extra_field">
<?php esc_html_e( 'Leave this field empty', 'rx-theme' ); ?>
</label>
<input type="text" name="rx_login_extra_field" id="rx_login_extra_field" value="" autocomplete="off" tabindex="-1" />
</p>
<?php
}
/**
* Check honeypot field.
*/
public function check_login_honeypot( $user, $username, $password ) {
if ( ! empty( $_POST['rx_login_extra_field'] ) ) {
$this->log_event(
'honeypot_block',
$username,
array(
'ip' => $this->get_ip(),
'ua' => $this->get_user_agent(),
)
);
return new WP_Error(
'rx_honeypot_block',
esc_html__( 'Login failed. Please try again.', 'rx-theme' )
);
}
return $user;
}
/**
* Block bad login requests.
*/
public function block_bad_login_requests() {
$ua = strtolower( $this->get_user_agent() );
$bad_agents = array(
'nikto',
'sqlmap',
'acunetix',
'nessus',
'masscan',
'nmap',
'zgrab',
'python-requests',
'libwww-perl',
'wget',
'curl',
);
foreach ( $bad_agents as $bad ) {
if ( false !== strpos( $ua, $bad ) ) {
$this->log_event(
'bad_user_agent_block',
'',
array(
'ip' => $this->get_ip(),
'ua' => $this->get_user_agent(),
)
);
status_header( 403 );
exit;
}
}
if ( isset( $_GET['action'] ) && 'register' === sanitize_key( $_GET['action'] ) ) {
$this->maybe_block_registration_by_option();
}
}
/**
* Respect WordPress registration option.
*/
private function maybe_block_registration_by_option() {
if ( ! get_option( 'users_can_register' ) ) {
status_header( 403 );
wp_die(
esc_html__( 'Registration is disabled.', 'rx-theme' ),
esc_html__( 'Forbidden', 'rx-theme' ),
array( 'response' => 403 )
);
}
}
/**
* Login page noindex.
*/
public function login_page_noindex() {
echo "<meta name='robots' content='noindex,nofollow,noarchive' />\n";
}
/**
* Disable XML-RPC.
*/
public function maybe_disable_xmlrpc( $enabled ) {
if ( RX_LOGIN_SECURITY_BLOCK_XMLRPC ) {
return false;
}
return $enabled;
}
/**
* Remove dangerous XML-RPC methods.
*/
public function remove_xmlrpc_methods( $methods ) {
if ( ! is_array( $methods ) ) {
return $methods;
}
$remove = array(
'pingback.ping',
'pingback.extensions.getPingbacks',
'wp.getUsersBlogs',
'wp.getUsers',
'wp.getProfile',
);
foreach ( $remove as $method ) {
if ( isset( $methods[ $method ] ) ) {
unset( $methods[ $method ] );
}
}
return $methods;
}
/**
* Block author enumeration.
*/
public function block_author_enumeration() {
if ( ! RX_LOGIN_SECURITY_BLOCK_AUTHOR_ENUM ) {
return;
}
if ( is_admin() ) {
return;
}
if ( isset( $_GET['author'] ) && ! empty( $_GET['author'] ) ) {
status_header( 403 );
wp_die(
esc_html__( 'Author enumeration is disabled.', 'rx-theme' ),
esc_html__( 'Forbidden', 'rx-theme' ),
array( 'response' => 403 )
);
}
}
/**
* Protect REST API user endpoints from public exposure.
*/
public function protect_rest_user_endpoints( $endpoints ) {
if ( ! RX_LOGIN_SECURITY_BLOCK_REST_USERS_PUBLIC ) {
return $endpoints;
}
if ( is_user_logged_in() && current_user_can( 'list_users' ) ) {
return $endpoints;
}
$protected = array(
'/wp/v2/users',
'/wp/v2/users/(?P<id>[\d]+)',
);
foreach ( $protected as $route ) {
if ( isset( $endpoints[ $route ] ) ) {
unset( $endpoints[ $route ] );
}
}
return $endpoints;
}
/**
* Send security headers.
*/
public function send_security_headers() {
if ( ! RX_LOGIN_SECURITY_ENABLE_HEADERS ) {
return;
}
if ( headers_sent() ) {
return;
}
header( 'X-Content-Type-Options: nosniff' );
header( 'X-Frame-Options: SAMEORIGIN' );
header( 'X-XSS-Protection: 1; mode=block' );
header( 'Referrer-Policy: strict-origin-when-cross-origin' );
header( 'Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=()' );
if ( is_ssl() ) {
header( 'Strict-Transport-Security: max-age=31536000; includeSubDomains; preload' );
}
}
/**
* Remove WP version exposure.
*/
public function remove_wp_version_exposure() {
if ( ! RX_LOGIN_SECURITY_HIDE_WP_VERSION ) {
return;
}
remove_action( 'wp_head', 'wp_generator' );
remove_action( 'rss2_head', 'the_generator' );
remove_action( 'commentsrss2_head', 'the_generator' );
remove_action( 'rss_head', 'the_generator' );
remove_action( 'rdf_header', 'the_generator' );
remove_action( 'atom_head', 'the_generator' );
remove_action( 'comments_atom_head', 'the_generator' );
}
/**
* Disable application passwords optionally.
*/
public function maybe_disable_application_passwords( $available ) {
if ( RX_LOGIN_SECURITY_DISABLE_APP_PASSWORDS ) {
return false;
}
return $available;
}
/**
* Validate stronger password on profile update.
*/
public function validate_profile_password_strength( $errors, $update, $user ) {
if ( ! RX_LOGIN_SECURITY_STRONG_PASSWORDS ) {
return;
}
if ( empty( $_POST['pass1'] ) ) {
return;
}
$password = (string) wp_unslash( $_POST['pass1'] );
$message = $this->password_strength_message( $password );
if ( $message ) {
$errors->add( 'rx_weak_password', $message );
}
}
/**
* Validate registration security.
*/
public function validate_registration_security( $errors, $sanitized_user_login, $user_email ) {
$username = strtolower( sanitize_user( $sanitized_user_login ) );
$blocked_usernames = array(
'admin',
'administrator',
'root',
'test',
'user',
'support',
'webmaster',
'info',
'owner',
'manager',
);
if ( in_array( $username, $blocked_usernames, true ) ) {
$errors->add(
'rx_blocked_username',
esc_html__( 'This username is not allowed. Please choose another username.', 'rx-theme' )
);
}
if ( ! is_email( $user_email ) ) {
$errors->add(
'rx_invalid_email',
esc_html__( 'Please enter a valid email address.', 'rx-theme' )
);
}
$domain = '';
if ( false !== strpos( $user_email, '@' ) ) {
$parts = explode( '@', $user_email );
$domain = strtolower( end( $parts ) );
}
$blocked_domains = apply_filters(
'rx_login_security_blocked_email_domains',
array(
'example.com',
'test.com',
'mailinator.com',
'tempmail.com',
'10minutemail.com',
'guerrillamail.com',
)
);
if ( $domain && in_array( $domain, $blocked_domains, true ) ) {
$errors->add(
'rx_blocked_email_domain',
esc_html__( 'This email domain is not allowed.', 'rx-theme' )
);
}
return $errors;
}
/**
* Password strength message.
*/
private function password_strength_message( $password ) {
$password = (string) $password;
if ( strlen( $password ) < 12 ) {
return esc_html__( 'Password must be at least 12 characters long.', 'rx-theme' );
}
if ( ! preg_match( '/[A-Z]/', $password ) ) {
return esc_html__( 'Password must include at least one uppercase letter.', 'rx-theme' );
}
if ( ! preg_match( '/[a-z]/', $password ) ) {
return esc_html__( 'Password must include at least one lowercase letter.', 'rx-theme' );
}
if ( ! preg_match( '/[0-9]/', $password ) ) {
return esc_html__( 'Password must include at least one number.', 'rx-theme' );
}
if ( ! preg_match( '/[^A-Za-z0-9]/', $password ) ) {
return esc_html__( 'Password must include at least one special character.', 'rx-theme' );
}
return '';
}
/**
* Send lockout email alert.
*/
private function maybe_send_lockout_email( $username, $data ) {
if ( ! RX_LOGIN_SECURITY_SEND_LOCKOUT_EMAIL ) {
return;
}
$admin_email = get_option( 'admin_email' );
if ( ! is_email( $admin_email ) ) {
return;
}
$subject = sprintf(
/* translators: %s: site name */
esc_html__( '[%s] Login lockout alert', 'rx-theme' ),
wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES )
);
$message = "RX Theme Login Security Alert\n\n";
$message .= "A login identity has been temporarily locked.\n\n";
$message .= 'Username: ' . sanitize_text_field( $username ) . "\n";
$message .= 'IP: ' . sanitize_text_field( $data['ip'] ?? '' ) . "\n";
$message .= 'User Agent: ' . sanitize_text_field( $data['ua'] ?? '' ) . "\n";
$message .= 'Attempts: ' . absint( $data['attempts'] ?? 0 ) . "\n";
$message .= 'Time: ' . current_time( 'mysql' ) . "\n";
wp_mail( $admin_email, $subject, $message );
}
/**
* Optional security logging.
*/
private function log_event( $event, $username = '', $context = array() ) {
if ( ! RX_LOGIN_SECURITY_LOG_EVENTS ) {
return;
}
$upload_dir = wp_upload_dir();
if ( empty( $upload_dir['basedir'] ) ) {
return;
}
$dir = trailingslashit( $upload_dir['basedir'] ) . 'rx-security-logs';
if ( ! wp_mkdir_p( $dir ) ) {
return;
}
$htaccess = trailingslashit( $dir ) . '.htaccess';
if ( ! file_exists( $htaccess ) ) {
file_put_contents( $htaccess, "Deny from all\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
}
$file = trailingslashit( $dir ) . 'login-security.log';
$line = wp_json_encode(
array(
'time' => current_time( 'mysql' ),
'event' => sanitize_key( $event ),
'username' => sanitize_user( $username ),
'ip' => $this->get_ip(),
'context' => $context,
)
);
if ( $line ) {
file_put_contents( $file, $line . PHP_EOL, FILE_APPEND | LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
}
}
}
endif;
RX_Login_Security::init();
Optional: control features from wp-config.php before WordPress loads:
define( 'RX_LOGIN_SECURITY_MAX_ATTEMPTS', 5 );
define( 'RX_LOGIN_SECURITY_LOCKOUT_SECONDS', 1800 );
define( 'RX_LOGIN_SECURITY_BLOCK_XMLRPC', true );
define( 'RX_LOGIN_SECURITY_DISABLE_APP_PASSWORDS', false );
define( 'RX_LOGIN_SECURITY_SEND_LOCKOUT_EMAIL', false );
define( 'RX_LOGIN_SECURITY_LOG_EVENTS', false );
define( 'RX_LOGIN_SECURITY_STRONG_PASSWORDS', true );
define( 'RX_LOGIN_SECURITY_ENABLE_HEADERS', true );
For your RX theme, this is a strong and safe login-security foundation. It should not break normal WordPress login, but after adding it, test login, logout, password reset, registration, REST API, and XML-RPC behavior.