diff --git a/README.md b/wp-security-pack/README.md similarity index 100% rename from README.md rename to wp-security-pack/README.md diff --git a/assets/css/admin.css b/wp-security-pack/assets/css/admin.css similarity index 100% rename from assets/css/admin.css rename to wp-security-pack/assets/css/admin.css diff --git a/assets/css/index.php b/wp-security-pack/assets/css/index.php similarity index 100% rename from assets/css/index.php rename to wp-security-pack/assets/css/index.php diff --git a/assets/index.php b/wp-security-pack/assets/index.php similarity index 100% rename from assets/index.php rename to wp-security-pack/assets/index.php diff --git a/data/.htaccess b/wp-security-pack/data/.htaccess similarity index 100% rename from data/.htaccess rename to wp-security-pack/data/.htaccess diff --git a/data/index.php b/wp-security-pack/data/index.php similarity index 100% rename from data/index.php rename to wp-security-pack/data/index.php diff --git a/includes/class-wpsp-activity-log.php b/wp-security-pack/includes/class-wpsp-activity-log.php similarity index 100% rename from includes/class-wpsp-activity-log.php rename to wp-security-pack/includes/class-wpsp-activity-log.php diff --git a/includes/class-wpsp-admin.php b/wp-security-pack/includes/class-wpsp-admin.php similarity index 99% rename from includes/class-wpsp-admin.php rename to wp-security-pack/includes/class-wpsp-admin.php index b66c4ca..5971a64 100644 --- a/includes/class-wpsp-admin.php +++ b/wp-security-pack/includes/class-wpsp-admin.php @@ -31,6 +31,7 @@ class WPSP_Admin { add_action( 'wp_ajax_wpsp_clear_logs', array( $this, 'ajax_clear_logs' ) ); add_action( 'wp_ajax_wpsp_export_logs', array( $this, 'ajax_export_logs' ) ); add_action( 'wp_ajax_wpsp_whitelist_ip', array( $this, 'ajax_whitelist_ip' ) ); + add_action( 'wp_ajax_wpsp_unblock_ip', array( $this, 'ajax_unblock_ip' ) ); add_action( 'wp_ajax_wpsp_clear_lockouts', array( $this, 'ajax_clear_lockouts' ) ); add_action( 'wp_ajax_wpsp_run_file_scan', array( $this, 'ajax_run_file_scan' ) ); add_action( 'wp_ajax_wpsp_run_malware_scan', array( $this, 'ajax_run_malware_scan' ) ); @@ -3018,6 +3019,30 @@ class WPSP_Admin { wp_send_json_success( array( 'csv' => implode( "\n", $csv_lines ) ) ); } + /** + * AJAX: Unblock a single IP. + */ + public function ajax_unblock_ip() { + check_ajax_referer( 'wpsp_admin' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-security-pack' ) ) ); + } + + $ip = isset( $_POST['ip'] ) ? sanitize_text_field( wp_unslash( $_POST['ip'] ) ) : ''; + + if ( empty( $ip ) || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid IP address.', 'wp-security-pack' ) ) ); + } + + $ip_control = wpsp()->get_component( 'ip_control' ); + if ( $ip_control ) { + $ip_control->unblock_ip( $ip ); + } + + wp_send_json_success(); + } + /** * AJAX: Clear all lockouts. */ diff --git a/includes/class-wpsp-db.php b/wp-security-pack/includes/class-wpsp-db.php similarity index 100% rename from includes/class-wpsp-db.php rename to wp-security-pack/includes/class-wpsp-db.php diff --git a/includes/class-wpsp-file-integrity.php b/wp-security-pack/includes/class-wpsp-file-integrity.php similarity index 100% rename from includes/class-wpsp-file-integrity.php rename to wp-security-pack/includes/class-wpsp-file-integrity.php diff --git a/includes/class-wpsp-geo-blocking.php b/wp-security-pack/includes/class-wpsp-geo-blocking.php similarity index 100% rename from includes/class-wpsp-geo-blocking.php rename to wp-security-pack/includes/class-wpsp-geo-blocking.php diff --git a/includes/class-wpsp-hardening.php b/wp-security-pack/includes/class-wpsp-hardening.php similarity index 100% rename from includes/class-wpsp-hardening.php rename to wp-security-pack/includes/class-wpsp-hardening.php diff --git a/includes/class-wpsp-helper.php b/wp-security-pack/includes/class-wpsp-helper.php similarity index 100% rename from includes/class-wpsp-helper.php rename to wp-security-pack/includes/class-wpsp-helper.php diff --git a/includes/class-wpsp-ip-control.php b/wp-security-pack/includes/class-wpsp-ip-control.php similarity index 100% rename from includes/class-wpsp-ip-control.php rename to wp-security-pack/includes/class-wpsp-ip-control.php diff --git a/includes/class-wpsp-ip2location.php b/wp-security-pack/includes/class-wpsp-ip2location.php similarity index 100% rename from includes/class-wpsp-ip2location.php rename to wp-security-pack/includes/class-wpsp-ip2location.php diff --git a/includes/class-wpsp-login-protection.php b/wp-security-pack/includes/class-wpsp-login-protection.php similarity index 95% rename from includes/class-wpsp-login-protection.php rename to wp-security-pack/includes/class-wpsp-login-protection.php index b9e278d..553945e 100644 --- a/includes/class-wpsp-login-protection.php +++ b/wp-security-pack/includes/class-wpsp-login-protection.php @@ -38,7 +38,6 @@ class WPSP_Login_Protection { // Priority 10 runs BEFORE WordPress's authenticate (priority 20) to block locked-out IPs early. add_filter( 'authenticate', array( $this, 'check_lockout' ), 10, 3 ); - // Successful login tracking. add_action( 'wp_login', array( $this, 'handle_successful_login' ), 10, 2 ); @@ -46,7 +45,29 @@ class WPSP_Login_Protection { // This runs on login_init which fires before the login form is displayed. add_action( 'login_init', array( $this, 'block_locked_out_on_login_page' ), 1 ); + // Honeypot field. + // NOTE: Must be registered BEFORE handle_custom_login_slug_early() which + // may load wp-login.php and exit, preventing any later hook registrations. + if ( WP_Security_Pack::get_setting( 'honeypot_enabled', true ) ) { + add_action( 'login_form', array( $this, 'add_honeypot_field' ) ); + add_action( 'register_form', array( $this, 'add_honeypot_field' ) ); + add_filter( 'authenticate', array( $this, 'check_honeypot' ), 1, 3 ); + add_filter( 'registration_errors', array( $this, 'check_honeypot_registration' ), 10, 3 ); + } + + // Hide login error messages (prevents username enumeration). + // NOTE: Must be registered BEFORE handle_custom_login_slug_early() which + // may load wp-login.php and exit, preventing any later hook registrations. + if ( WP_Security_Pack::get_setting( 'hide_login_errors', true ) ) { + // Replace specific auth errors at the source (runs after WP's authenticate at priority 20). + add_filter( 'authenticate', array( $this, 'genericize_auth_error' ), PHP_INT_MAX, 3 ); + // Also filter the rendered error string as a safety net. + add_filter( 'login_errors', array( $this, 'hide_login_errors' ), PHP_INT_MAX ); + } + // Custom login URL - must run very early before WordPress processes the request. + // WARNING: handle_custom_login_slug_early() may require wp-login.php and exit. + // All login-related hooks MUST be registered above this point. if ( WP_Security_Pack::get_setting( 'login_rename_enabled', false ) ) { $this->custom_login_slug = WP_Security_Pack::get_setting( 'login_custom_url', '' ); if ( ! empty( $this->custom_login_slug ) ) { @@ -67,19 +88,6 @@ class WPSP_Login_Protection { $this->block_wp_admin_early(); add_action( 'init', array( $this, 'hide_wp_admin' ), 1 ); } - - // Honeypot field. - if ( WP_Security_Pack::get_setting( 'honeypot_enabled', true ) ) { - add_action( 'login_form', array( $this, 'add_honeypot_field' ) ); - add_action( 'register_form', array( $this, 'add_honeypot_field' ) ); - add_filter( 'authenticate', array( $this, 'check_honeypot' ), 1, 3 ); - add_filter( 'registration_errors', array( $this, 'check_honeypot_registration' ), 10, 3 ); - } - - // Hide login error messages (prevents username enumeration). - if ( WP_Security_Pack::get_setting( 'hide_login_errors', true ) ) { - add_filter( 'login_errors', array( $this, 'hide_login_errors' ) ); - } } /** @@ -358,6 +366,33 @@ class WPSP_Login_Protection { 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. * diff --git a/includes/class-wpsp-malware-scanner.php b/wp-security-pack/includes/class-wpsp-malware-scanner.php similarity index 100% rename from includes/class-wpsp-malware-scanner.php rename to wp-security-pack/includes/class-wpsp-malware-scanner.php diff --git a/includes/class-wpsp-two-factor.php b/wp-security-pack/includes/class-wpsp-two-factor.php similarity index 100% rename from includes/class-wpsp-two-factor.php rename to wp-security-pack/includes/class-wpsp-two-factor.php diff --git a/includes/index.php b/wp-security-pack/includes/index.php similarity index 100% rename from includes/index.php rename to wp-security-pack/includes/index.php diff --git a/index.php b/wp-security-pack/index.php similarity index 100% rename from index.php rename to wp-security-pack/index.php diff --git a/languages/index.php b/wp-security-pack/languages/index.php similarity index 100% rename from languages/index.php rename to wp-security-pack/languages/index.php diff --git a/readme.txt b/wp-security-pack/readme.txt similarity index 100% rename from readme.txt rename to wp-security-pack/readme.txt diff --git a/uninstall.php b/wp-security-pack/uninstall.php similarity index 100% rename from uninstall.php rename to wp-security-pack/uninstall.php diff --git a/wp-security-pack-logo.png b/wp-security-pack/wp-security-pack-logo.png similarity index 100% rename from wp-security-pack-logo.png rename to wp-security-pack/wp-security-pack-logo.png diff --git a/wp-security-pack.php b/wp-security-pack/wp-security-pack.php similarity index 100% rename from wp-security-pack.php rename to wp-security-pack/wp-security-pack.php