From 5f91b979eb293b8674072fb31bc930f43b2882e9 Mon Sep 17 00:00:00 2001 From: Malin Date: Fri, 13 Mar 2026 07:08:50 +0100 Subject: [PATCH] feat: protect WP login, password reset, and fix Elementor AJAX bypass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move elementor_pro/forms/validation hook before is_admin() early return (same AJAX bypass bug as CF7 — Elementor submits to admin-ajax.php) - Add login_head + login_footer hooks so CSS/JS HMAC token loads on wp-login.php (wp_head/footer do not fire on that page) - Add lostpassword_form + woocommerce_lostpassword_form injection hooks - Add authenticate filter (validate_wp_login) for WP native login, guarded to skip WC login and non-form auth calls - Add lostpassword_post action (validate_lost_password) for password reset, covering both WP and WC My Account lost-password forms - Exclude woocommerce-lost-password-nonce from generic catch-all to avoid double-processing WC lost-password submissions Co-Authored-By: Claude Sonnet 4.6 --- honeypot-fields.php | 53 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/honeypot-fields.php b/honeypot-fields.php index 1c9a10a..88607ec 100644 --- a/honeypot-fields.php +++ b/honeypot-fields.php @@ -1195,9 +1195,10 @@ class SmartHoneypotAntiSpam { /* ── Init ──────────────────────────────────────────────────────── */ public function init() { - // CF7 submits via admin-ajax.php where is_admin()=true, so hook here - // before the early return so spam validation still runs. + // CF7 and Elementor submit via admin-ajax.php where is_admin()=true, + // so register their validation hooks before the early return. add_filter('wpcf7_spam', [$this, 'validate_cf7_spam'], 10, 2); + add_action('elementor_pro/forms/validation', [$this, 'validate_elementor_form'], 10, 2); if (is_admin()) { add_action('admin_notices', [$this, 'activation_notice']); @@ -1213,6 +1214,8 @@ class SmartHoneypotAntiSpam { add_action('woocommerce_after_order_notes',[$this, 'echo_honeypot']); add_action('register_form', [$this, 'echo_honeypot']); add_action('login_form', [$this, 'echo_honeypot']); + add_action('lostpassword_form', [$this, 'echo_honeypot']); + add_action('woocommerce_lostpassword_form',[$this, 'echo_honeypot']); add_action('elementor_pro/forms/render_field', [$this, 'add_to_elementor_form'], 10, 2); add_action('elementor/widget/render_content', [$this, 'filter_elementor_widget'], 10, 2); add_filter('gform_form_tag', [$this, 'add_to_gravity_forms'], 10, 2); @@ -1224,13 +1227,16 @@ class SmartHoneypotAntiSpam { add_filter('woocommerce_process_login_errors', [$this, 'validate_wc_login'], 10, 3); add_action('woocommerce_after_checkout_validation', [$this, 'validate_wc_checkout'], 10, 2); add_filter('registration_errors', [$this, 'validate_wp_registration'], 10, 3); + add_filter('authenticate', [$this, 'validate_wp_login'], 20, 3); + add_action('lostpassword_post', [$this, 'validate_lost_password'], 10, 2); add_filter('preprocess_comment', [$this, 'validate_comment']); - add_action('elementor_pro/forms/validation', [$this, 'validate_elementor_form'], 10, 2); add_action('template_redirect', [$this, 'validate_generic_post']); - // CSS & JS - add_action('wp_head', [$this, 'print_css']); - add_action('wp_footer', [$this, 'print_js'], 99); + // CSS & JS (wp-login.php uses login_head/login_footer, not wp_head/footer) + add_action('wp_head', [$this, 'print_css']); + add_action('wp_footer', [$this, 'print_js'], 99); + add_action('login_head', [$this, 'print_css']); + add_action('login_footer', [$this, 'print_js'], 99); } /* ── Honeypot HTML ─────────────────────────────────────────────── */ @@ -1417,6 +1423,38 @@ class SmartHoneypotAntiSpam { return $errors; } + public function validate_wp_login($user, $username, $password) { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + return $user; + } + // Only validate if our honeypot was injected (fields present in POST) + if (!isset($_POST[$this->hp_name]) && !isset($_POST[$this->token_name])) { + return $user; + } + // WooCommerce login has its own validator + if (isset($_POST['woocommerce-login-nonce'])) { + return $user; + } + $this->current_form_type = 'WP Login'; + if (!$this->check_submission(true)) { + return new \WP_Error( + 'honeypot_spam', + __('Error: Login blocked. Enable JavaScript and try again.', 'smart-honeypot') + ); + } + return $user; + } + + public function validate_lost_password($errors, $user_data) { + $this->current_form_type = 'WP Password Reset'; + if (!$this->check_submission(true)) { + $errors->add( + 'honeypot_spam', + __('Error: Request blocked. Enable JavaScript and try again.', 'smart-honeypot') + ); + } + } + public function validate_comment($commentdata) { if (is_user_logged_in() && current_user_can('moderate_comments')) { $this->clean_post_data(); @@ -1462,7 +1500,8 @@ class SmartHoneypotAntiSpam { isset($_POST['woocommerce-process-checkout-nonce']) || isset($_POST['comment_post_ID']) || isset($_POST['_wpcf7']) || // CF7: handled by wpcf7_spam filter - (isset($_POST['action']) && $_POST['action'] === 'elementor_pro_forms_send_form') + (isset($_POST['action']) && $_POST['action'] === 'elementor_pro_forms_send_form') || + isset($_POST['woocommerce-lost-password-nonce']) // WC lost password: handled by lostpassword_post ) { return; }