commit c2a29ffbbef98d0d5a06c7e60509b5303f72a494 Author: Malin Date: Mon Feb 16 09:54:17 2026 +0100 feat: honeypot fields v2.0 with JS challenge, rate limiting, and proper WooCommerce hooks - Require honeypot field presence (blocks direct POST bots) - Add HMAC-based JavaScript proof-of-work token via SubtleCrypto - Hook into woocommerce_process_registration_errors for proper validation - Add IP-based rate limiting (3 registrations/hour) via transients - Add timestamp validation (min 3s, max 2h) - Use realistic field names to avoid bot detection - Support WP core registration, comments, Elementor, Gravity Forms, CF7 diff --git a/honeypot-fields.php b/honeypot-fields.php new file mode 100644 index 0000000..6a1be21 --- /dev/null +++ b/honeypot-fields.php @@ -0,0 +1,542 @@ +secret = wp_hash('honeypot_plugin_secret_v2'); + + // Field names look like legitimate form fields to trick bots + $this->hp_name = 'website_url_confirm'; + $this->token_name = 'form_session_id'; + $this->time_name = 'form_render_ts'; + + add_action('init', [$this, 'init']); + } + + /* ------------------------------------------------------------------ + * INIT — register all hooks + * ----------------------------------------------------------------*/ + public function init() { + if (is_admin()) { + add_action('admin_notices', [$this, 'activation_notice']); + return; + } + + // --- Inject honeypot into forms --- + + // WordPress core + add_filter('the_content', [$this, 'add_to_content_forms'], 99); + add_filter('comment_form_defaults', [$this, 'add_to_comment_form_defaults']); + add_action('comment_form_after_fields', [$this, 'echo_honeypot']); + add_action('comment_form_logged_in_after', [$this, 'echo_honeypot']); + + // WooCommerce + add_action('woocommerce_register_form', [$this, 'echo_honeypot']); + add_action('woocommerce_login_form', [$this, 'echo_honeypot']); + add_action('woocommerce_after_order_notes', [$this, 'echo_honeypot']); + + // WordPress registration + add_action('register_form', [$this, 'echo_honeypot']); + add_action('login_form', [$this, 'echo_honeypot']); + + // Elementor Pro forms + 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); + + // Gravity Forms + add_filter('gform_form_tag', [$this, 'add_to_gravity_forms'], 10, 2); + + // Contact Form 7 + add_filter('wpcf7_form_elements', [$this, 'add_to_cf7']); + + // Generic form search + add_filter('get_search_form', [$this, 'add_to_search_form'], 99); + + // --- Validate on POST --- + + // WooCommerce registration (proper hook) + add_filter('woocommerce_process_registration_errors', [$this, 'validate_wc_registration'], 10, 4); + + // WooCommerce login + add_filter('woocommerce_process_login_errors', [$this, 'validate_wc_login'], 10, 3); + + // WooCommerce checkout + add_action('woocommerce_after_checkout_validation', [$this, 'validate_wc_checkout'], 10, 2); + + // WordPress core registration + add_filter('registration_errors', [$this, 'validate_wp_registration'], 10, 3); + + // Comments + add_filter('preprocess_comment', [$this, 'validate_comment']); + + // Elementor Pro forms + add_action('elementor_pro/forms/validation', [$this, 'validate_elementor_form'], 10, 2); + + // Generic early POST check for other forms + 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); + } + + /* ------------------------------------------------------------------ + * HONEYPOT FIELD HTML + * ----------------------------------------------------------------*/ + private function get_honeypot_html() { + $ts = time(); + $token_data = $ts . '|' . wp_create_nonce('hp_form_' . $ts); + + // The wrapper uses a generic class name + return sprintf( + '', + esc_attr($this->hp_name), + esc_attr($this->token_name), + esc_attr($this->time_name), + esc_attr($ts) + ); + } + + /** Echo the honeypot (for action hooks) */ + public function echo_honeypot() { + echo $this->get_honeypot_html(); + } + + /* ------------------------------------------------------------------ + * INJECTION HELPERS + * ----------------------------------------------------------------*/ + public function add_to_content_forms($content) { + if (is_admin() || is_feed()) { + return $content; + } + return preg_replace_callback( + '/(]*>)(.*?)(<\/form>)/is', + function ($m) { + return $m[1] . $m[2] . $this->get_honeypot_html() . $m[3]; + }, + $content + ); + } + + public function add_to_comment_form_defaults($defaults) { + if (isset($defaults['fields']['url'])) { + // Honeypot already has a URL-looking field; inject after existing fields + } + return $defaults; + } + + public function add_to_search_form($form) { + return preg_replace( + '/(<\/form>)/i', + $this->get_honeypot_html() . '$1', + $form, + 1 + ); + } + + public function add_to_elementor_form($field, $instance) { + static $done = false; + if (!$done && $field['type'] === 'submit') { + $done = true; + echo $this->get_honeypot_html(); + } + } + + public function filter_elementor_widget($content, $widget) { + if ($widget->get_name() === 'form') { + $content = preg_replace( + '/(<\/form>)/i', + $this->get_honeypot_html() . '$1', + $content, + 1 + ); + } + return $content; + } + + public function add_to_gravity_forms($tag, $form) { + return preg_replace( + '/(
validate_js_token($_POST[$this->token_name])) { + $this->log_spam('JS token invalid'); + return false; + } + } + + // 3. Timestamp check + if ($require_fields && !isset($_POST[$this->time_name])) { + $this->log_spam('Timestamp field missing'); + return false; + } + if (isset($_POST[$this->time_name])) { + $ts = intval($_POST[$this->time_name]); + $now = time(); + $diff = $now - $ts; + if ($diff < self::MIN_SUBMIT_TIME) { + $this->log_spam("Submitted too fast ({$diff}s)"); + return false; + } + if ($diff > self::MAX_SUBMIT_TIME) { + $this->log_spam("Timestamp too old ({$diff}s)"); + return false; + } + } + + // Clean up honeypot fields so downstream code never sees them + $this->clean_post_data(); + + return true; + } + + /** Validate the JS-generated token */ + private function validate_js_token(string $token): bool { + // Token format: "nonce_value" set by JS using the hidden field + // JS computes: HMAC of (timestamp + site-specific value) + // We verify it matches what we'd expect + $parts = explode('|', $token); + if (count($parts) !== 2) { + return false; + } + $ts = intval($parts[0]); + $hash = $parts[1]; + $expected = hash_hmac('sha256', $ts . '|honeypot_js_proof', $this->secret); + return hash_equals(substr($expected, 0, 16), $hash); + } + + /** Remove honeypot fields from $_POST / $_REQUEST */ + private function clean_post_data() { + foreach ([$this->hp_name, $this->token_name, $this->time_name] as $key) { + unset($_POST[$key], $_REQUEST[$key]); + } + } + + private function log_spam(string $reason) { + $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; + $uri = $_SERVER['REQUEST_URI'] ?? 'unknown'; + $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'; + error_log("[Honeypot] SPAM blocked: {$reason} | IP: {$ip} | URI: {$uri} | UA: {$ua}"); + } + + /* ------------------------------------------------------------------ + * RATE LIMITING + * ----------------------------------------------------------------*/ + private function check_rate_limit(): bool { + $ip = $_SERVER['REMOTE_ADDR'] ?? ''; + if (!$ip) { + return false; + } + $key = 'hp_rate_' . md5($ip); + $count = (int) get_transient($key); + if ($count >= self::RATE_LIMIT) { + $this->log_spam("Rate limit exceeded ({$count} attempts) for IP {$ip}"); + return false; + } + set_transient($key, $count + 1, HOUR_IN_SECONDS); + return true; + } + + /* ------------------------------------------------------------------ + * WOOCOMMERCE VALIDATION + * ----------------------------------------------------------------*/ + public function validate_wc_registration($errors, $username, $password, $email) { + if (!$this->check_submission(true)) { + $errors->add( + 'honeypot_spam', + __('Error: Registration blocked. If you are not a bot, please enable JavaScript and try again.', 'smart-honeypot') + ); + return $errors; + } + if (!$this->check_rate_limit()) { + $errors->add( + 'honeypot_rate', + __('Error: Too many registration attempts. Please try again later.', 'smart-honeypot') + ); + } + return $errors; + } + + public function validate_wc_login($errors, $username, $password) { + if (!$this->check_submission(true)) { + $errors->add( + 'honeypot_spam', + __('Error: Login blocked. Please enable JavaScript and try again.', 'smart-honeypot') + ); + } + return $errors; + } + + public function validate_wc_checkout($data, $errors) { + if (!$this->check_submission(false)) { + $errors->add( + 'honeypot_spam', + __('Order could not be processed. Please try again.', 'smart-honeypot') + ); + } + } + + /* ------------------------------------------------------------------ + * WORDPRESS CORE VALIDATION + * ----------------------------------------------------------------*/ + public function validate_wp_registration($errors, $sanitized_user_login, $user_email) { + if (!$this->check_submission(true)) { + $errors->add( + 'honeypot_spam', + __('Error: Registration blocked. Please enable JavaScript and try again.', 'smart-honeypot') + ); + } + if (!$this->check_rate_limit()) { + $errors->add( + 'honeypot_rate', + __('Error: Too many attempts. Please try again later.', 'smart-honeypot') + ); + } + return $errors; + } + + public function validate_comment($commentdata) { + // Don't check admins/editors + if (is_user_logged_in() && current_user_can('moderate_comments')) { + $this->clean_post_data(); + return $commentdata; + } + if (!$this->check_submission(true)) { + wp_die( + __('Comment blocked as spam. Please enable JavaScript and try again.', 'smart-honeypot'), + __('Spam Detected', 'smart-honeypot'), + ['response' => 403, 'back_link' => true] + ); + } + return $commentdata; + } + + /* ------------------------------------------------------------------ + * ELEMENTOR VALIDATION + * ----------------------------------------------------------------*/ + public function validate_elementor_form($record, $ajax_handler) { + if (!$this->check_submission(true)) { + $ajax_handler->add_error('honeypot', __('Spam detected. Please try again.', 'smart-honeypot')); + } + } + + /* ------------------------------------------------------------------ + * GENERIC POST VALIDATION (catch-all for other forms) + * ----------------------------------------------------------------*/ + public function validate_generic_post() { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + return; + } + + // Only validate if our honeypot fields are present (don't interfere + // with forms we didn't inject into, admin-ajax, REST API, etc.) + if (!isset($_POST[$this->hp_name]) && !isset($_POST[$this->token_name])) { + return; + } + + // Skip if already handled by a specific hook above + if ( + isset($_POST['woocommerce-register-nonce']) || + isset($_POST['woocommerce-login-nonce']) || + isset($_POST['woocommerce-process-checkout-nonce']) || + isset($_POST['comment_post_ID']) || + (isset($_POST['action']) && $_POST['action'] === 'elementor_pro_forms_send_form') + ) { + return; + } + + if (!$this->check_submission(false)) { + if (wp_doing_ajax()) { + wp_send_json_error(['message' => __('Spam detected.', 'smart-honeypot')]); + } + $ref = wp_get_referer(); + wp_safe_redirect($ref ?: home_url()); + exit; + } + } + + /* ------------------------------------------------------------------ + * CSS + * ----------------------------------------------------------------*/ + public function print_css() { + // Use a nondescript class name + echo ''; + } + + /* ------------------------------------------------------------------ + * JS — generates the proof-of-work token + * ----------------------------------------------------------------*/ + public function print_js() { + $secret = esc_js($this->secret); + $token_name = esc_js($this->token_name); + $time_name = esc_js($this->time_name); + + echo << +(function(){ + function computeToken(ts, secret) { + // Simple HMAC-like hash using SubtleCrypto + var msg = ts + '|honeypot_js_proof'; + var encoder = new TextEncoder(); + return crypto.subtle.importKey( + 'raw', encoder.encode(secret), {name:'HMAC',hash:'SHA-256'}, false, ['sign'] + ).then(function(key){ + return crypto.subtle.sign('HMAC', key, encoder.encode(msg)); + }).then(function(sig){ + var arr = Array.from(new Uint8Array(sig)); + var hex = arr.map(function(b){return b.toString(16).padStart(2,'0')}).join(''); + return hex.substring(0, 16); + }); + } + + function fillTokens() { + var forms = document.querySelectorAll('input[name="{$token_name}"]'); + forms.forEach(function(input){ + var tsInput = input.parentElement.querySelector('input[name="{$time_name}"]'); + var ts = tsInput ? tsInput.value : Math.floor(Date.now()/1000).toString(); + computeToken(ts, '{$secret}').then(function(hash){ + input.value = ts + '|' + hash; + }); + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', fillTokens); + } else { + fillTokens(); + } + + // Re-fill for dynamically added forms + var obs = new MutationObserver(function(mutations){ + var found = false; + mutations.forEach(function(m){ + m.addedNodes.forEach(function(n){ + if (n.nodeType === 1 && (n.querySelector && n.querySelector('input[name="{$token_name}"]'))) { + found = true; + } + }); + }); + if (found) fillTokens(); + }); + obs.observe(document.body, {childList:true, subtree:true}); +})(); + +JSBLOCK; + } + + /* ------------------------------------------------------------------ + * ADMIN NOTICE + * ----------------------------------------------------------------*/ + public function activation_notice() { + if (get_transient('smart_honeypot_activated')) { + echo '
+

Honeypot Fields is now active. All forms are protected automatically.

+
'; + delete_transient('smart_honeypot_activated'); + } + } +} + +// Boot +add_action('plugins_loaded', function () { + new SmartHoneypotAntiSpam(); +}); + +// Activation +register_activation_hook(__FILE__, function () { + set_transient('smart_honeypot_activated', true, 30); +}); + +// Deactivation +register_deactivation_hook(__FILE__, function () { + delete_transient('smart_honeypot_activated'); +}); + +// Settings link +add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) { + array_unshift($links, 'Documentation'); + return $links; +});