custom_login_slug = WP_Security_Pack::get_setting( 'login_custom_url', '' ); if ( ! empty( $this->custom_login_slug ) ) { // Handle custom login slug immediately (constructor runs during plugins_loaded). $this->handle_custom_login_slug_early(); // These hooks can run on init. add_action( 'init', array( $this, 'restrict_wp_login' ), 1 ); add_filter( 'site_url', array( $this, 'filter_login_url' ), 10, 4 ); add_filter( 'wp_redirect', array( $this, 'filter_redirect_url' ), 10, 2 ); add_filter( 'login_url', array( $this, 'filter_login_url_direct' ), 10, 3 ); } } // Hide wp-admin for non-logged-in users. if ( WP_Security_Pack::get_setting( 'hide_wp_admin', false ) ) { // Must run early - WordPress redirects wp-admin to wp-login.php before init. $this->block_wp_admin_early(); add_action( 'init', array( $this, 'hide_wp_admin' ), 1 ); } } /** * Block locked out IPs from seeing the login page. * Runs on login_init before the form is displayed. */ public function block_locked_out_on_login_page() { // Check if custom login URL is enabled and block direct wp-login.php access. if ( $this->should_block_direct_wp_login() ) { $this->redirect_to_404(); } // Check lockout first. if ( $this->is_ip_locked_out_early() ) { $this->show_lockout_message_early(); } // Check admin access restriction. if ( $this->is_admin_access_restricted() ) { $this->show_admin_access_denied_message(); } } /** * Check if direct wp-login.php access should be blocked. * * @return bool True if access should be blocked. */ private function should_block_direct_wp_login() { // Only applies when custom login URL is enabled. if ( ! WP_Security_Pack::get_setting( 'login_rename_enabled', false ) ) { return false; } $custom_slug = WP_Security_Pack::get_setting( 'login_custom_url', '' ); if ( empty( $custom_slug ) ) { return false; } // Allow logged-in users (for logout, profile actions, etc.). $logged_in = false; foreach ( $_COOKIE as $name => $value ) { if ( strpos( $name, 'wordpress_logged_in_' ) === 0 ) { $logged_in = true; break; } } if ( $logged_in ) { return false; } // Allow if user has the access cookie (visited custom login URL before). // This allows all WordPress flows (password reset, etc.) to work normally. $expected_value = wp_hash( 'wpsp_login_' . $custom_slug ); $cookie_value = isset( $_COOKIE['wpsp_login_access'] ) ? $_COOKIE['wpsp_login_access'] : ''; if ( $cookie_value === $expected_value ) { return false; } // No valid cookie - block direct wp-login.php access. return true; } /** * Check if admin access is restricted for current IP/country. * * @return bool True if access should be denied. */ private function is_admin_access_restricted() { if ( ! WP_Security_Pack::get_setting( 'admin_access_restriction', false ) ) { return false; } $ip = WPSP_Helper::get_client_ip(); if ( ! $ip ) { return false; } // Check if IP is whitelisted (always allow). $ip_control = wpsp()->get_component( 'ip_control' ); if ( ! $ip_control ) { $ip_control = new WPSP_IP_Control(); } if ( $ip_control->is_whitelisted( $ip ) ) { return false; } $allowed_countries = WP_Security_Pack::get_setting( 'admin_allowed_countries', array() ); $allowed_ips = WP_Security_Pack::get_setting( 'admin_allowed_ips', '' ); $allowed_ip_list = WPSP_Helper::parse_ip_list( $allowed_ips ); $has_country_restriction = ! empty( $allowed_countries ); $has_ip_restriction = ! empty( $allowed_ip_list ); // If no restrictions configured, allow access. if ( ! $has_country_restriction && ! $has_ip_restriction ) { return false; } $access_granted = false; // Check IP restriction. if ( $has_ip_restriction && WPSP_Helper::ip_matches_rules( $ip, $allowed_ip_list ) ) { $access_granted = true; } // Check country restriction. if ( ! $access_granted && $has_country_restriction ) { $geo_blocking = wpsp()->get_component( 'geo_blocking' ); if ( ! $geo_blocking ) { $geo_blocking = new WPSP_Geo_Blocking(); } $country_code = $geo_blocking->get_country_code( $ip ); if ( $country_code && in_array( $country_code, $allowed_countries, true ) ) { $access_granted = true; } } // If access not granted, it should be restricted. return ! $access_granted; } /** * Show admin access denied message and exit. */ private function show_admin_access_denied_message() { $ip = WPSP_Helper::get_client_ip(); // Log the blocked access attempt. WPSP_Activity_Log::log( WPSP_Activity_Log::EVENT_IP_BLOCKED, $ip, null, __( 'Admin access denied: country/IP not allowed', 'wp-security-pack' ) ); status_header( 403 ); nocache_headers(); wp_die( esc_html__( 'Admin access is not permitted from your location.', 'wp-security-pack' ), esc_html__( 'Access Denied', 'wp-security-pack' ), array( 'response' => 403, 'back_link' => false, ) ); } /** * Handle failed login attempt. * * @param string $username Username attempted. */ public function handle_failed_login( $username ) { if ( ! WP_Security_Pack::get_setting( 'login_limit_enabled', true ) ) { return; } $ip = WPSP_Helper::get_client_ip(); if ( ! $ip ) { return; } // Check if IP is whitelisted. $ip_control = wpsp()->get_component( 'ip_control' ); if ( $ip_control && $ip_control->is_whitelisted( $ip ) ) { WPSP_Activity_Log::log( WPSP_Activity_Log::EVENT_LOGIN_FAILED, $ip, $username, __( 'Failed login (whitelisted IP)', 'wp-security-pack' ) ); return; } // Log the failed attempt. WPSP_Activity_Log::log( WPSP_Activity_Log::EVENT_LOGIN_FAILED, $ip, $username ); // Increment failed attempts counter. $attempts = $this->increment_failed_attempts( $ip ); // Check if lockout threshold reached. $max_attempts = WP_Security_Pack::get_setting( 'login_max_attempts', 5 ); if ( $attempts >= $max_attempts ) { $this->lockout_ip( $ip ); } // Send email alert if enabled. $this->maybe_send_alert( 'failed_login', $ip, $username, $attempts ); } /** * Handle successful login. * * @param string $user_login Username. * @param WP_User $user User object. */ public function handle_successful_login( $user_login, $user ) { $ip = WPSP_Helper::get_client_ip(); // Log the successful login. WPSP_Activity_Log::log( WPSP_Activity_Log::EVENT_LOGIN_SUCCESS, $ip, $user_login ); // Clear failed attempts for this IP. $this->clear_failed_attempts( $ip ); // Send admin login notification if enabled. if ( WP_Security_Pack::get_setting( 'admin_login_notify', false ) && user_can( $user, 'manage_options' ) ) { $this->send_admin_login_notification( $user, $ip ); } } /** * Send notification when an admin logs in. * * @param WP_User $user User object. * @param string $ip IP address. */ private function send_admin_login_notification( $user, $ip ) { $email = WP_Security_Pack::get_setting( 'email_alerts_address', get_option( 'admin_email' ) ); if ( empty( $email ) ) { return; } // Check if this is a new IP for this user. $known_ips = get_user_meta( $user->ID, '_wpsp_known_ips', true ); if ( ! is_array( $known_ips ) ) { $known_ips = array(); } // Only notify for new IPs (or always if no IPs recorded yet). $is_new_ip = ! in_array( $ip, $known_ips, true ); // Add IP to known list (keep last 10). if ( $is_new_ip ) { $known_ips[] = $ip; $known_ips = array_slice( $known_ips, -10 ); update_user_meta( $user->ID, '_wpsp_known_ips', $known_ips ); } // Only send notification for new IPs. if ( ! $is_new_ip ) { return; } $site_name = get_bloginfo( 'name' ); $subject = sprintf( /* translators: %s: Site name */ __( '[%s] Admin Login from New IP', 'wp-security-pack' ), $site_name ); $message = sprintf( /* translators: 1: Username, 2: Site name */ __( 'An administrator account logged in to %2$s from a new IP address.', 'wp-security-pack' ), $user->user_login, $site_name ) . "\n\n"; $message .= sprintf( __( 'Username: %s', 'wp-security-pack' ), $user->user_login ) . "\n"; $message .= sprintf( __( 'IP Address: %s', 'wp-security-pack' ), $ip ) . "\n"; $message .= sprintf( __( 'Time: %s', 'wp-security-pack' ), current_time( 'mysql' ) ) . "\n\n"; $message .= __( 'If this was not you, please secure your account immediately.', 'wp-security-pack' ) . "\n"; wp_mail( $email, $subject, $message ); } /** * Hide specific login error messages to prevent username enumeration. * * @param string $error Error message. * @return string */ public function hide_login_errors( $error ) { // Return generic message instead of revealing if username or password was wrong. return __( 'Invalid username or password.', 'wp-security-pack' ); } /** * Replace specific authentication errors with a generic message. * * Runs at very late priority on the authenticate filter to catch WordPress's * own specific error messages (invalid_username, incorrect_password, etc.) * and replace them before they reach the login page rendering. * * @param WP_User|WP_Error|null $user User object or error. * @param string $username Username. * @param string $password Password. * @return WP_User|WP_Error|null */ public function genericize_auth_error( $user, $username, $password ) { if ( is_wp_error( $user ) ) { $specific_codes = array( 'invalid_username', 'invalid_email', 'incorrect_password', 'invalidcombo' ); if ( in_array( $user->get_error_code(), $specific_codes, true ) ) { return new WP_Error( 'authentication_failed', __( 'Invalid username or password.', 'wp-security-pack' ) ); } } return $user; } /** * Check if IP is locked out before authentication. * * @param WP_User|WP_Error|null $user User object or error. * @param string $username Username. * @param string $password Password. * @return WP_User|WP_Error|null */ public function check_lockout( $user, $username, $password ) { if ( ! WP_Security_Pack::get_setting( 'login_limit_enabled', true ) ) { return $user; } if ( empty( $username ) ) { return $user; } $ip = WPSP_Helper::get_client_ip(); if ( ! $ip ) { return $user; } // Check if IP is whitelisted. $ip_control = wpsp()->get_component( 'ip_control' ); if ( $ip_control && $ip_control->is_whitelisted( $ip ) ) { return $user; } // Check if IP is locked out. $lockout = $this->get_lockout( $ip ); $max_attempts = (int) WP_Security_Pack::get_setting( 'login_max_attempts', 5 ); $lockout_duration = (int) WP_Security_Pack::get_setting( 'login_lockout_duration', 15 ); if ( $lockout && (int) $lockout->failed_attempts >= $max_attempts ) { // Calculate lockout expiry from updated_at + duration. // Use current_time('timestamp') for WordPress timezone consistency. $updated_time = strtotime( $lockout->updated_at ); $lockout_expires = $updated_time + ( $lockout_duration * 60 ); $current_time = current_time( 'timestamp' ); if ( $current_time < $lockout_expires ) { // Still locked out. $remaining = human_time_diff( $current_time, $lockout_expires ); return new WP_Error( 'wpsp_locked_out', sprintf( /* translators: %s: Time remaining */ __( 'Too many failed login attempts. Please try again in %s.', 'wp-security-pack' ), $remaining ) ); } else { // Lockout expired - clear the record so they can try again. $this->clear_failed_attempts( $ip ); } } return $user; } /** * Increment failed attempts counter. * * @param string $ip IP address. * @return int New count. */ private function increment_failed_attempts( $ip ) { global $wpdb; $table = WPSP_DB::get_lockout_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $existing = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT * FROM {$table} WHERE ip_address = %s", $ip ) ); if ( $existing ) { $new_count = $existing->failed_attempts + 1; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->update( $table, array( 'failed_attempts' => $new_count, 'updated_at' => current_time( 'mysql' ), ), array( 'ip_address' => $ip ), array( '%d', '%s' ), array( '%s' ) ); return $new_count; } // Create new record. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->insert( $table, array( 'ip_address' => $ip, 'failed_attempts' => 1, 'created_at' => current_time( 'mysql' ), ), array( '%s', '%d', '%s' ) ); return 1; } /** * Clear failed attempts for an IP. * * @param string $ip IP address. */ private function clear_failed_attempts( $ip ) { global $wpdb; $table = WPSP_DB::get_lockout_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->delete( $table, array( 'ip_address' => $ip ), array( '%s' ) ); } /** * Lockout an IP address. * * @param string $ip IP address. */ private function lockout_ip( $ip ) { global $wpdb; $table = WPSP_DB::get_lockout_table(); $duration = (int) WP_Security_Pack::get_setting( 'login_lockout_duration', 15 ); // Store Unix timestamp to avoid timezone issues. $lockout_until = time() + ( $duration * 60 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->update( $table, array( 'lockout_until' => $lockout_until, 'updated_at' => current_time( 'mysql' ), ), array( 'ip_address' => $ip ), array( '%d', '%s' ), array( '%s' ) ); // Log the lockout. WPSP_Activity_Log::log( WPSP_Activity_Log::EVENT_LOCKOUT, $ip, null, sprintf( /* translators: %d: Duration in minutes */ __( 'Locked out for %d minutes', 'wp-security-pack' ), $duration ) ); // Send email alert. $this->maybe_send_alert( 'lockout', $ip ); // Check for auto-blacklist of repeat offenders. $this->maybe_auto_blacklist( $ip ); } /** * Check if IP should be auto-blacklisted for repeat lockouts. * * @param string $ip IP address. */ private function maybe_auto_blacklist( $ip ) { if ( ! WP_Security_Pack::get_setting( 'auto_blacklist_enabled', false ) ) { return; } $threshold = (int) WP_Security_Pack::get_setting( 'auto_blacklist_threshold', 3 ); if ( $threshold < 1 ) { return; } // Count lockouts for this IP in the activity log. $lockout_count = $this->count_ip_lockouts( $ip ); if ( $lockout_count >= $threshold ) { $this->add_ip_to_blacklist( $ip ); // Log the auto-blacklist. WPSP_Activity_Log::log( WPSP_Activity_Log::EVENT_IP_BLOCKED, $ip, null, sprintf( /* translators: %d: Number of lockouts */ __( 'Auto-blacklisted after %d lockouts', 'wp-security-pack' ), $lockout_count ) ); // Send email alert. $this->maybe_send_alert( 'auto_blacklist', $ip, $lockout_count ); } } /** * Count the number of lockouts for an IP address. * * @param string $ip IP address. * @return int */ private function count_ip_lockouts( $ip ) { global $wpdb; $table = WPSP_DB::get_log_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $count = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT COUNT(*) FROM {$table} WHERE ip_address = %s AND event_type = %s", $ip, WPSP_Activity_Log::EVENT_LOCKOUT ) ); return (int) $count; } /** * Add an IP address to the blacklist. * * @param string $ip IP address. */ private function add_ip_to_blacklist( $ip ) { $current_blacklist = WP_Security_Pack::get_setting( 'ip_blacklist', '' ); // Check if IP is already in the blacklist. $blacklist_array = array_filter( array_map( 'trim', explode( "\n", $current_blacklist ) ) ); if ( in_array( $ip, $blacklist_array, true ) ) { return; } // Add IP to blacklist. $blacklist_array[] = $ip; $new_blacklist = implode( "\n", $blacklist_array ); WP_Security_Pack::update_setting( 'ip_blacklist', $new_blacklist ); // Clear the IP control cache. $ip_control = wpsp()->get_component( 'ip_control' ); if ( $ip_control ) { $ip_control->clear_cache(); } } /** * Get lockout record for an IP. * * @param string $ip IP address. * @return object|null */ private function get_lockout( $ip ) { global $wpdb; $table = WPSP_DB::get_lockout_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery return $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT * FROM {$table} WHERE ip_address = %s", $ip ) ); } /** * Block wp-admin access early - called from constructor. * Runs before WordPress can redirect to wp-login.php. */ private function block_wp_admin_early() { // Check if user is logged in - we can't use is_user_logged_in() this early. // Check for the logged_in cookie instead. $logged_in = false; foreach ( $_COOKIE as $name => $value ) { if ( strpos( $name, 'wordpress_logged_in_' ) === 0 ) { $logged_in = true; break; } } if ( $logged_in ) { return; } // Check if requesting wp-admin. $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : ''; $is_wp_admin = strpos( $request_uri, '/wp-admin' ) !== false; $is_ajax = strpos( $request_uri, 'admin-ajax.php' ) !== false; if ( $is_wp_admin && ! $is_ajax ) { // Redirect to a non-existent URL to trigger WordPress's themed 404 page. $home = get_option( 'home', '' ); $fake_url = rtrim( $home, '/' ) . '/wpsp-404-' . mt_rand( 1000, 9999 ); header( 'Location: ' . $fake_url, true, 302 ); exit; } } /** * Handle access to the custom login slug - called early from constructor. * Directly loads wp-login.php when accessed via custom slug. */ public function handle_custom_login_slug_early() { if ( ! $this->is_custom_login_slug_request_early() ) { return; } // Check if IP is locked out before showing login form. if ( $this->is_ip_locked_out_early() ) { $this->show_lockout_message_early(); } // Check admin access restriction before showing login form. if ( $this->is_admin_access_restricted() ) { $this->show_admin_access_denied_message(); } // Set a flag that user accessed via custom login slug. // This will be checked by restrict_wp_login on subsequent requests. $cookie_value = wp_hash( 'wpsp_login_' . $this->custom_login_slug ); // Define cookie constants if not already defined. if ( ! defined( 'COOKIEPATH' ) ) { define( 'COOKIEPATH', '/' ); } if ( ! defined( 'COOKIE_DOMAIN' ) ) { define( 'COOKIE_DOMAIN', '' ); } setcookie( $this->cookie_name, $cookie_value, time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true ); // Also set it in the superglobal for immediate availability. $_COOKIE[ $this->cookie_name ] = $cookie_value; // Directly load the login page (like WPS Hide Login does). require_once ABSPATH . 'wp-login.php'; exit; } /** * Check if IP is locked out - early version that works before init. * * @return bool */ private function is_ip_locked_out_early() { if ( ! WP_Security_Pack::get_setting( 'login_limit_enabled', true ) ) { return false; } $ip = WPSP_Helper::get_client_ip(); if ( ! $ip ) { return false; } // Check whitelist first. $ip_control = wpsp()->get_component( 'ip_control' ); if ( $ip_control && $ip_control->is_whitelisted( $ip ) ) { return false; } // Check lockout status directly from database. global $wpdb; $table = WPSP_DB::get_lockout_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $lockout = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT * FROM {$table} WHERE ip_address = %s", $ip ) ); if ( ! $lockout ) { return false; } $max_attempts = (int) WP_Security_Pack::get_setting( 'login_max_attempts', 5 ); $lockout_minutes = (int) WP_Security_Pack::get_setting( 'login_lockout_duration', 15 ); $current_time = time(); // Check if IP has reached max attempts and is still within lockout window. if ( (int) $lockout->failed_attempts >= $max_attempts && ! empty( $lockout->updated_at ) ) { $updated_time = strtotime( $lockout->updated_at ); $lockout_expires = $updated_time + ( $lockout_minutes * 60 ); if ( $current_time < $lockout_expires ) { return true; } } // Also check lockout_until for honeypot/manual blocks. if ( ! empty( $lockout->lockout_until ) && is_numeric( $lockout->lockout_until ) ) { if ( $current_time < (int) $lockout->lockout_until ) { return true; } } return false; } /** * Show lockout message and exit - early version. */ private function show_lockout_message_early() { $lockout_minutes = (int) WP_Security_Pack::get_setting( 'login_lockout_duration', 15 ); status_header( 403 ); nocache_headers(); wp_die( sprintf( /* translators: %d: Lockout duration in minutes */ esc_html__( 'Too many failed login attempts. Please try again in %d minutes.', 'wp-security-pack' ), $lockout_minutes ), esc_html__( 'Access Denied', 'wp-security-pack' ), array( 'response' => 403, 'back_link' => false, ) ); } /** * Check if the current request is for the custom login slug - early version. * Uses native PHP functions to avoid dependency on WordPress functions. * * @return bool */ private function is_custom_login_slug_request_early() { $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : ''; $request_path = trim( parse_url( $request_uri, PHP_URL_PATH ), '/' ); // Get the site path for subdirectory installations. // Use get_option directly since home_url() might have filters. $home = get_option( 'home', '' ); $home_path = trim( parse_url( $home, PHP_URL_PATH ), '/' ); // Remove the home path prefix from the request path. if ( ! empty( $home_path ) && strpos( $request_path, $home_path ) === 0 ) { $request_path = trim( substr( $request_path, strlen( $home_path ) ), '/' ); } return $request_path === $this->custom_login_slug; } /** * Restrict direct access to wp-login.php. * Block unless user has the access cookie from custom login slug. */ public function restrict_wp_login() { // Check if we're on wp-login.php using the reliable $pagenow global. if ( ! isset( $GLOBALS['pagenow'] ) || 'wp-login.php' !== $GLOBALS['pagenow'] ) { return; } // Allow logged-in users. if ( is_user_logged_in() ) { return; } // Allow if user has the access cookie (came through custom login slug before). $expected_value = wp_hash( 'wpsp_login_' . $this->custom_login_slug ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $cookie_value = isset( $_COOKIE[ $this->cookie_name ] ) ? $_COOKIE[ $this->cookie_name ] : ''; if ( $cookie_value === $expected_value ) { return; // Cookie is valid, allow access. } // No valid cookie - block access with 404. $this->redirect_to_404(); } /** * Check if the current request is for the custom login slug. * * @return bool */ private function is_custom_login_slug_request() { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; $request_path = trim( wp_parse_url( $request_uri, PHP_URL_PATH ), '/' ); // Get the site path for subdirectory installations. $home_path = trim( wp_parse_url( home_url(), PHP_URL_PATH ), '/' ); // Remove the home path prefix from the request path. if ( ! empty( $home_path ) && strpos( $request_path, $home_path ) === 0 ) { $request_path = trim( substr( $request_path, strlen( $home_path ) ), '/' ); } return $request_path === $this->custom_login_slug; } /** * Redirect to 404 page. */ private function redirect_to_404() { // Try to get the site's 404 page URL. $home_url = home_url( '/' ); // Use a non-existent URL to trigger WordPress 404. $fake_404_url = home_url( '/wpsp-not-found-' . wp_rand( 1000, 9999 ) . '/' ); status_header( 404 ); nocache_headers(); wp_safe_redirect( $fake_404_url, 302 ); exit; } /** * Hide wp-admin for non-logged-in users. */ public function hide_wp_admin() { if ( is_user_logged_in() ) { return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; // Check if trying to access wp-admin (works for both root and subdirectory installs). // Allow admin-ajax.php for frontend AJAX functionality. $is_wp_admin = strpos( $request_uri, '/wp-admin' ) !== false; $is_ajax = strpos( $request_uri, 'admin-ajax.php' ) !== false; if ( $is_wp_admin && ! $is_ajax ) { $this->redirect_to_404(); } } /** * Filter login URL in site_url. * * @param string $url The URL. * @param string $path The path. * @param string|null $scheme The scheme. * @param int|null $blog_id Blog ID. * @return string */ public function filter_login_url( $url, $path, $scheme, $blog_id ) { if ( strpos( $url, 'wp-login.php' ) !== false && ! empty( $this->custom_login_slug ) ) { $url = str_replace( 'wp-login.php', $this->custom_login_slug, $url ); } return $url; } /** * Filter login URL directly. * * @param string $login_url The login URL. * @param string $redirect The redirect URL. * @param bool $force_reauth Force reauth. * @return string */ public function filter_login_url_direct( $login_url, $redirect, $force_reauth ) { if ( ! empty( $this->custom_login_slug ) ) { $login_url = str_replace( 'wp-login.php', $this->custom_login_slug, $login_url ); } return $login_url; } /** * Filter redirect URLs. * * @param string $location Redirect location. * @param int $status HTTP status. * @return string */ public function filter_redirect_url( $location, $status ) { if ( strpos( $location, 'wp-login.php' ) !== false && ! empty( $this->custom_login_slug ) ) { // Check the redirect_to parameter in the location URL. $redirect_to = ''; if ( preg_match( '/redirect_to=([^&]+)/', $location, $matches ) ) { $redirect_to = urldecode( $matches[1] ); } // If redirect_to contains wp-admin and hide_wp_admin is enabled, block with 404. // This prevents /wp-admin from revealing the custom login URL. // Use get_option directly to avoid any potential issues with class method. $settings = get_option( 'wpsp_settings', array() ); $hide_wp_admin = ! empty( $settings['hide_wp_admin'] ); $is_wp_admin_redirect = strpos( $redirect_to, 'wp-admin' ) !== false && strpos( $redirect_to, 'admin-ajax.php' ) === false; if ( $hide_wp_admin && $is_wp_admin_redirect ) { // Block with 404 - don't reveal the custom login URL. status_header( 404 ); nocache_headers(); // Exit immediately to prevent the redirect. exit; } $location = str_replace( 'wp-login.php', $this->custom_login_slug, $location ); } return $location; } /** * Show 404 page. */ private function show_404() { global $wp_query; status_header( 404 ); nocache_headers(); if ( $wp_query ) { $wp_query->set_404(); } // Try to load theme 404 template. $template = get_404_template(); if ( $template ) { include $template; } else { wp_die( esc_html__( 'Page not found.', 'wp-security-pack' ), esc_html__( '404 Not Found', 'wp-security-pack' ), array( 'response' => 404 ) ); } exit; } /** * Add honeypot field to login form. */ public function add_honeypot_field() { // Honeypot field - hidden via CSS, bots will fill it. ?>
get_component( 'ip_control' ); $ban_duration = (int) WP_Security_Pack::get_setting( 'honeypot_ban_duration', 60 ); if ( $ip_control ) { $ip_control->auto_block_ip( $ip, $ban_duration, __( 'Honeypot triggered', 'wp-security-pack' ) ); } return new WP_Error( 'wpsp_honeypot', __( 'Authentication failed.', 'wp-security-pack' ) ); } return $user; } /** * Check honeypot field during registration. * * @param WP_Error $errors Registration errors. * @param string $sanitized_user_login User login. * @param string $user_email User email. * @return WP_Error */ public function check_honeypot_registration( $errors, $sanitized_user_login, $user_email ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! empty( $_POST['wpsp_hp_email'] ) ) { $errors->add( 'wpsp_honeypot', __( 'Registration failed.', 'wp-security-pack' ) ); // Auto-block this IP using configurable duration. $ip = WPSP_Helper::get_client_ip(); $ip_control = wpsp()->get_component( 'ip_control' ); $ban_duration = (int) WP_Security_Pack::get_setting( 'honeypot_ban_duration', 60 ); if ( $ip_control && $ip ) { $ip_control->auto_block_ip( $ip, $ban_duration, __( 'Honeypot triggered on registration', 'wp-security-pack' ) ); } } return $errors; } /** * Maybe send email alert. * * @param string $type Alert type. * @param string $ip IP address. * @param string $username Username (optional). * @param int $attempts Number of attempts (optional). */ private function maybe_send_alert( $type, $ip, $username = '', $attempts = 0 ) { if ( ! WP_Security_Pack::get_setting( 'email_alerts_enabled', false ) ) { return; } $email = WP_Security_Pack::get_setting( 'email_alerts_address', get_option( 'admin_email' ) ); if ( empty( $email ) ) { return; } $site_name = get_bloginfo( 'name' ); $site_url = home_url(); switch ( $type ) { case 'failed_login': $threshold = WP_Security_Pack::get_setting( 'email_alert_threshold', 3 ); if ( $attempts < $threshold ) { return; } $subject = sprintf( /* translators: %s: Site name */ __( '[%s] Multiple Failed Login Attempts', 'wp-security-pack' ), $site_name ); $message = sprintf( /* translators: 1: Number of attempts, 2: IP address, 3: Username, 4: Site URL */ __( "There have been %1\$d failed login attempts on your site.\n\nIP Address: %2\$s\nUsername attempted: %3\$s\nSite: %4\$s\n\nIf this wasn't you, the IP will be automatically locked out after reaching the threshold.", 'wp-security-pack' ), $attempts, $ip, $username ? $username : __( '(empty)', 'wp-security-pack' ), $site_url ); break; case 'lockout': $subject = sprintf( /* translators: %s: Site name */ __( '[%s] IP Address Locked Out', 'wp-security-pack' ), $site_name ); $message = sprintf( /* translators: 1: IP address, 2: Site URL */ __( "An IP address has been locked out due to too many failed login attempts.\n\nIP Address: %1\$s\nSite: %2\$s", 'wp-security-pack' ), $ip, $site_url ); break; case 'auto_blacklist': $subject = sprintf( /* translators: %s: Site name */ __( '[%s] IP Permanently Blacklisted', 'wp-security-pack' ), $site_name ); $message = sprintf( /* translators: 1: IP address, 2: Number of lockouts, 3: Site URL */ __( "An IP address has been permanently added to your blacklist due to repeated lockouts.\n\nIP Address: %1\$s\nTotal Lockouts: %2\$d\nSite: %3\$s\n\nThis IP will no longer be able to access your website.", 'wp-security-pack' ), $ip, $attempts, $site_url ); break; default: return; } wp_mail( $email, $subject, $message ); } }