<?php /** * Optimized challenge mechanisms for BotFend Anti-Bot Firewall * * @package BotFend\Challenges * @version 4.9.0 - Extracted inline CSS/JS to external files, consolidated to single JS */ namespace BotFend\Challenges; use BotFend\Core\Config; use BotFend\Utilities\Utilities; use BotFend\Database\Database; defined('ABSPATH') || exit; /** * Class Challenges * * Handles JavaScript challenges, honeypots, and bot verification */ class Challenges { private $config; private $utilities; private $database; private $whitelisted_ips = []; private $whitelisted_paths = []; private $challenge_probability = 30; private $headless_allowlist = []; const CHALLENGE_EXPIRY = 300; const VERIFY_EXPIRY = 300; const HONEYPOT_WINDOW = 300; const UA_SIMILARITY_THRESHOLD = 90; public function __construct(Utilities $utilities, Database $database) { $this->config = Config::get_instance(); $this->utilities = $utilities; $this->database = $database; $this->load_whitelist(); $this->load_whitelisted_paths(); $this->load_headless_allowlist(); $this->challenge_probability = $this->config->get_int('challenge_probability', 30); add_action('init', [$this, 'check_honeypot_hit']); add_action('wp_ajax_botfend_verify_challenge', [$this, 'verify_challenge']); add_action('wp_ajax_nopriv_botfend_verify_challenge', [$this, 'verify_challenge']); } /** * Enqueue the single combined challenges JS file. * Safe to call multiple times  only registers/enqueues once. */ private function enqueue_challenges_js(): void { if (!wp_script_is('botfend-challenges-js', 'registered')) { wp_register_script( 'botfend-challenges-js', BOTFEND_ASSETS_URL . 'js/botfend-challenges.js', [], BOTFEND_VERSION, true ); } wp_enqueue_script('botfend-challenges-js'); } private function load_whitelist() { $whitelist_option = $this->config->get_string('whitelist_ips', ''); $this->whitelisted_ips = array_filter(array_map('trim', explode("\n", $whitelist_option))); $this->whitelisted_ips = array_map('sanitize_text_field', $this->whitelisted_ips); } private function load_whitelisted_paths() { $paths_raw = $this->config->get_string('whitelisted_paths', ''); $this->whitelisted_paths = array_filter(array_map('trim', explode("\n", $paths_raw))); $this->whitelisted_paths = array_map('sanitize_text_field', $this->whitelisted_paths); } private function load_headless_allowlist() { $this->headless_allowlist = [ 'pingdom', 'uptimerobot', 'statuscake', 'newrelic', 'datadog', 'sitespeed.io', 'lighthouse', 'pagespeed', 'gtmetrix', 'wave', 'axe', 'pa11y', 'siteimprove', 'tenon', 'google page speed', 'google lite', 'chrome-lighthouse', ]; $this->headless_allowlist = apply_filters('botfend_headless_allowlist', $this->headless_allowlist); } private function is_ip_whitelisted($ip) { if (empty($this->whitelisted_ips)) { return false; } foreach ($this->whitelisted_ips as $whitelist_ip) { if (strpos($whitelist_ip, '#') === 0) { continue; } if (strpos($whitelist_ip, '/') !== false) { if ($this->ip_in_cidr($ip, $whitelist_ip)) { return true; } } elseif ($ip === $whitelist_ip) { return true; } } return false; } private function ip_in_cidr($ip, $cidr) { if (strpos($ip, ':') !== false) { return $this->ipv6_in_cidr($ip, $cidr); } list($subnet, $bits) = explode('/', $cidr); $ip_long = ip2long($ip); $subnet_long = ip2long($subnet); if ($ip_long === false || $subnet_long === false) { return false; } $bits = intval($bits); if ($bits < 0 || $bits > 32) { return false; } $mask = -1 << (32 - $bits); $subnet_long &= $mask; return ($ip_long & $mask) == $subnet_long; } private function ipv6_in_cidr($ip, $cidr) { if (!function_exists('inet_pton')) { return false; } list($subnet, $bits) = explode('/', $cidr); $ip_bin = inet_pton($ip); $subnet_bin = inet_pton($subnet); if ($ip_bin === false || $subnet_bin === false) { return false; } $bits = intval($bits); if ($bits < 0 || $bits > 128) { return false; } $mask = ''; $full_bytes = floor($bits / 8); $remaining_bits = $bits % 8; for ($i = 0; $i < $full_bytes; $i++) { $mask .= chr(255); } if ($remaining_bits > 0) { $mask .= chr(bindec(str_pad(str_repeat('1', $remaining_bits), 8, '0'))); } $mask = str_pad($mask, 16, chr(0)); for ($i = 0; $i < 16; $i++) { $ip_byte = ord($ip_bin[$i]); $subnet_byte = ord($subnet_bin[$i]); $mask_byte = ord($mask[$i]); if (($ip_byte & $mask_byte) !== ($subnet_byte & $mask_byte)) { return false; } } return true; } private function is_path_whitelisted($path) { if (empty($path)) { return false; } foreach ($this->whitelisted_paths as $whitelisted_path) { if ($path === $whitelisted_path || (substr($whitelisted_path, -1) === '/' && strpos($path, $whitelisted_path) === 0)) { return true; } } if (strpos($path, '/.well-known/') === 0) { return true; } return false; } private function extract_browser_family($ua) { $ua = strtolower($ua); if (strpos($ua, 'edg/') !== false || strpos($ua, 'edge/') !== false) { return 'edge'; } if (strpos($ua, 'opr/') !== false || strpos($ua, 'opera/') !== false) { return 'opera'; } if (strpos($ua, 'chrome/') !== false && strpos($ua, 'edg/') === false) { return 'chrome'; } if (strpos($ua, 'firefox/') !== false) { return 'firefox'; } if (strpos($ua, 'safari/') !== false && strpos($ua, 'chrome/') === false) { return 'safari'; } if (strpos($ua, 'trident/') !== false || strpos($ua, 'msie ') !== false) { return 'ie'; } return 'unknown'; } private function extract_os_family($ua) { $ua = strtolower($ua); if (strpos($ua, 'windows nt') !== false) { return 'windows'; } if (strpos($ua, 'mac os x') !== false) { return 'macos'; } if (strpos($ua, 'linux') !== false) { return 'linux'; } if (strpos($ua, 'android') !== false) { return 'android'; } if (strpos($ua, 'ios') !== false || strpos($ua, 'iphone') !== false || strpos($ua, 'ipad') !== false) { return 'ios'; } return 'unknown'; } private function extract_major_version($ua) { $ua = strtolower($ua); $patterns = [ 'chrome/([0-9]+)', 'firefox/([0-9]+)', 'safari/([0-9]+)', 'version/([0-9]+)', 'msie ([0-9]+)', 'rv:([0-9]+)', 'edge/([0-9]+)', 'opr/([0-9]+)', ]; foreach ($patterns as $pattern) { if (preg_match('/' . $pattern . '/i', $ua, $matches)) { return $matches[1]; } } return '0'; } private function create_ua_fingerprint($user_agent) { $browser = $this->extract_browser_family($user_agent); $version = $this->extract_major_version($user_agent); $os = $this->extract_os_family($user_agent); return md5($browser . '|' . $version . '|' . $os); } private function verify_user_agent($stored_fingerprint, $current_ua) { $current_fingerprint = $this->create_ua_fingerprint($current_ua); return hash_equals($stored_fingerprint, $current_fingerprint); } private function detect_headless_browser($user_agent, $ip) { if ($this->utilities->verify_legitimate_crawler($ip, $user_agent)) { return false; } $ua_lower = strtolower($user_agent); foreach ($this->headless_allowlist as $allowed) { if (strpos($ua_lower, $allowed) !== false) { return false; } } $headless_patterns = [ 'headless', 'phantomjs', 'puppeteer', 'selenium', 'playwright', 'chrome-headless', 'headlesschrome', 'headless-chrome' ]; foreach ($headless_patterns as $pattern) { if (stripos($user_agent, $pattern) !== false) { if ($this->is_monitoring_service($ip, $user_agent)) { return false; } return 'suspicious'; } } if (empty($user_agent) || $this->utilities->has_suspicious_headers()) { return 'suspicious'; } return false; } private function is_monitoring_service($ip, $user_agent) { $monitoring_ranges = [ '107.6.0.0/16', '204.14.80.0/22', '69.162.124.0/24', '216.144.250.150/32', '104.236.0.0/16', '198.58.0.0/16', ]; foreach ($monitoring_ranges as $range) { if ($this->ip_in_cidr($ip, $range)) { return true; } } return false; } public function issue_js_challenge($ip) { if ($this->is_ip_whitelisted($ip)) { return; } $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''; if ($this->is_path_whitelisted($request_uri)) { return; } if (!$this->config->get_bool('enable_js_challenge', true)) { return; } $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; if ($this->utilities->verify_legitimate_crawler($ip, $user_agent)) { return; } $headless_result = $this->detect_headless_browser($user_agent, $ip); if ($headless_result === true) { $country = $this->utilities->get_country($ip); $block_duration = $this->config->get_int('headless_block_duration', 24); $this->database->block_ip( sanitize_text_field($ip), 'headless_bot', esc_html__('Headless browser detected', 'botfend-anti-bot-firewall'), 10, $block_duration, sanitize_text_field($country) ); $this->database->log_request( $ip, 10, 'headless_bot_blocked', [esc_html__('Headless browser detected and blocked', 'botfend-anti-bot-firewall')], $user_agent, false, $country ); $this->render_blocked_page($ip, 'headless'); exit; } elseif ($headless_result === 'suspicious') { $challenge_key = 'botfend_challenge_count_' . md5($ip); $challenge_count = absint(get_transient($challenge_key) ?: 0); $challenge_rate_limit = $this->config->get_int('challenge_rate_limit', 5); if ($challenge_count > $challenge_rate_limit) { $country = $this->utilities->get_country($ip); $block_duration = $this->config->get_int('challenge_abuse_block_duration', 12); $this->database->block_ip( sanitize_text_field($ip), 'challenge_abuse', esc_html__('Excessive challenge requests', 'botfend-anti-bot-firewall'), 8, $block_duration, sanitize_text_field($country) ); return; } $challenge_count++; set_transient($challenge_key, $challenge_count, self::CHALLENGE_EXPIRY); } if (wp_rand(0, 100) < $this->challenge_probability) { $challenge_token = $this->generate_secure_token($ip); $ua_fingerprint = $this->create_ua_fingerprint($user_agent); $challenge_data = [ 'token' => $challenge_token, 'ip' => $ip, 'expires' => time() + self::CHALLENGE_EXPIRY, 'created' => time(), 'ua_fingerprint' => $ua_fingerprint, 'original_ua' => $user_agent, 'attempts' => 0 ]; set_transient('botfend_challenge_' . md5($challenge_token), $challenge_data, self::CHALLENGE_EXPIRY); $country = $this->utilities->get_country($ip); $this->database->log_request($ip, 7, 'js_challenge', [esc_html__('JavaScript challenge issued', 'botfend-anti-bot-firewall')], $user_agent, false, $country); $this->render_js_challenge($ip, $challenge_token); exit; } } private function generate_secure_token($ip) { $data = [ 'ip' => $ip, 'time' => time(), 'random' => bin2hex(random_bytes(16)), 'salt' => wp_salt('nonce') ]; $token_data = wp_json_encode($data); return hash_hmac('sha256', $token_data, wp_salt('auth')); } private function render_blocked_page($ip, $reason = 'headless') { nocache_headers(); header('X-Frame-Options: DENY'); header('X-Content-Type-Options: nosniff'); header('Referrer-Policy: strict-origin-when-cross-origin'); if (is_ssl()) { header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?php esc_html_e('Access Denied', 'botfend-anti-bot-firewall'); ?></title> <?php wp_enqueue_style('botfend-block-css', BOTFEND_ASSETS_URL . 'css/botfend-block.css', [], BOTFEND_VERSION); ?> <?php wp_print_styles('botfend-block-css'); ?> </head> <body class="botfend-block-page"> <div class="botfend-container"> <div class="icon" style="font-size: 64px; margin-bottom: 20px; color: #dc3545;">&Ô</div> <h1><?php esc_html_e('Access Denied', 'botfend-anti-bot-firewall'); ?></h1> <p><?php esc_html_e('Automated browsing tools are not allowed on this site.', 'botfend-anti-bot-firewall'); ?></p> <p><small><?php esc_html_e('If you believe this is an error, please contact the site administrator.', 'botfend-anti-bot-firewall'); ?></small></p> </div> </body> </html> <?php exit; } private function render_js_challenge($ip, $token) { nocache_headers(); header('X-Frame-Options: DENY'); header('X-Content-Type-Options: nosniff'); header('Referrer-Policy: strict-origin-when-cross-origin'); if (is_ssl()) { header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); } // Enqueue external CSS wp_enqueue_style( 'botfend-challenges-css', BOTFEND_ASSETS_URL . 'css/botfend-challenges.css', [], BOTFEND_VERSION ); wp_enqueue_style( 'botfend-block-css', BOTFEND_ASSETS_URL . 'css/botfend-block.css', [], BOTFEND_VERSION ); // Enqueue combined JS with challenge data $this->enqueue_challenges_js(); wp_add_inline_script( 'botfend-challenges-js', 'var botfendChallenge = ' . wp_json_encode([ 'ajaxUrl' => admin_url('admin-ajax.php'), 'token' => $token, 'nonce' => wp_create_nonce('botfend_challenge'), ]) . ';', 'before' ); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title><?php esc_html_e('Verifying Browser...', 'botfend-anti-bot-firewall'); ?></title> <?php wp_print_styles(['botfend-challenges-css', 'botfend-block-css']); ?> </head> <body class="botfend-challenge-page"> <div class="botfend-container botfend-challenge-container"> <div class="botfend-spinner-container"> <div class="botfend-spinner"></div> </div> <h2><?php esc_html_e('Verifying Your Browser', 'botfend-anti-bot-firewall'); ?></h2> <p><?php esc_html_e('Please wait while we verify your browser is not automated.', 'botfend-anti-bot-firewall'); ?></p> <div class="botfend-progress-bar"> <div class="botfend-progress-fill" id="botfend-progress-fill"></div> </div> <p> <small> <span class="botfend-security-badge">Ø=Ý <?php esc_html_e('Security Check', 'botfend-anti-bot-firewall'); ?></span> <br> <a href="#" id="botfend-learn-more-link" class="botfend-learn-more-link"> <?php esc_html_e('Why am I seeing this?', 'botfend-anti-bot-firewall'); ?> </a> </small> </p> <div id="botfend-explanation-box" class="botfend-explanation-box"> <strong><?php esc_html_e('About this security check:', 'botfend-anti-bot-firewall'); ?></strong> <p style="margin: 10px 0 0 0; font-size: 14px;"> <?php esc_html_e('This check helps protect our website from malicious bots and automated attacks. It ensures that you are a real human visitor. The verification only takes a few seconds and helps keep our site safe for everyone.', 'botfend-anti-bot-firewall'); ?> </p> </div> <form method="POST" action="<?php echo esc_url(admin_url('admin-ajax.php')); ?>" class="botfend-hidden" id="botfend-challenge-fallback"> <input type="hidden" name="action" value="botfend_verify_challenge"> <input type="hidden" name="token" value="<?php echo esc_attr($token); ?>"> <input type="hidden" name="nonce" value="<?php echo esc_attr(wp_create_nonce('botfend_challenge')); ?>"> <button type="submit" class="botfend-btn"><?php esc_html_e('Continue', 'botfend-anti-bot-firewall'); ?></button> </form> </div> <?php wp_print_scripts('botfend-challenges-js'); ?> </body> </html> <?php } public function verify_challenge() { check_ajax_referer('botfend_challenge', 'nonce'); $token = isset($_POST['token']) ? sanitize_text_field(wp_unslash($_POST['token'])) : ''; $ip = $this->utilities->get_client_ip(); $verification_key = 'botfend_verify_attempts_' . md5($ip . $token); $attempts = absint(get_transient($verification_key) ?: 0); if ($attempts > 3) { wp_send_json_error(['message' => esc_html__('Too many attempts', 'botfend-anti-bot-firewall')]); } $attempts++; set_transient($verification_key, $attempts, self::VERIFY_EXPIRY); $challenge_key = 'botfend_challenge_' . md5($token); $challenge = get_transient($challenge_key); if (!$challenge) { wp_send_json_error(['message' => esc_html__('Challenge expired or invalid', 'botfend-anti-bot-firewall')]); } if (!hash_equals($challenge['token'], $token)) { wp_send_json_error(['message' => esc_html__('Invalid token', 'botfend-anti-bot-firewall')]); } if ($challenge['ip'] !== $ip) { wp_send_json_error(['message' => esc_html__('IP mismatch', 'botfend-anti-bot-firewall')]); } if ($challenge['expires'] < time()) { delete_transient($challenge_key); wp_send_json_error(['message' => esc_html__('Challenge expired', 'botfend-anti-bot-firewall')]); } $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; $current_fingerprint = $this->create_ua_fingerprint($user_agent); if ($challenge['ua_fingerprint'] !== $current_fingerprint) { if (isset($challenge['original_ua'])) { $similarity = 0; similar_text($challenge['original_ua'], $user_agent, $similarity); if ($similarity < self::UA_SIMILARITY_THRESHOLD) { delete_transient($challenge_key); wp_send_json_error(['message' => esc_html__('User agent mismatch', 'botfend-anti-bot-firewall')]); } } else { delete_transient($challenge_key); wp_send_json_error(['message' => esc_html__('User agent mismatch', 'botfend-anti-bot-firewall')]); } } if ($challenge['attempts'] > 2) { delete_transient($challenge_key); wp_send_json_error(['message' => esc_html__('Too many attempts', 'botfend-anti-bot-firewall')]); } $challenge['attempts']++; set_transient($challenge_key, $challenge, self::CHALLENGE_EXPIRY); $verified_key = 'botfend_verified_' . md5($ip); set_transient($verified_key, [ 'verified' => true, 'verified_at' => time(), 'ip' => $ip ], DAY_IN_SECONDS); delete_transient($challenge_key); delete_transient($verification_key); $country = $this->utilities->get_country($ip); $this->database->log_request($ip, 0, 'challenge_passed', [esc_html__('JavaScript challenge completed', 'botfend-anti-bot-firewall')], $user_agent, false, $country); $redirect = isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : home_url(); $redirect_host = wp_parse_url($redirect, PHP_URL_HOST); $site_host = wp_parse_url(home_url(), PHP_URL_HOST); if ($redirect_host && $redirect_host !== $site_host) { $redirect = home_url(); } wp_send_json_success([ 'message' => esc_html__('Verified', 'botfend-anti-bot-firewall'), 'redirect' => $redirect ]); } public function has_passed_challenge() { $ip = $this->utilities->get_client_ip(); $verified_key = 'botfend_verified_' . md5($ip); $verified = get_transient($verified_key); return $verified && !empty($verified['verified']); } public function add_honeypots() { $ip = $this->utilities->get_client_ip(); $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''; if ($this->is_ip_whitelisted($ip) || $this->is_path_whitelisted($request_uri)) { return; } if (!$this->config->get_bool('enable_honeypot', true)) { return; } if (is_admin() || wp_doing_ajax()) { return; } echo '<div style="display:none !important; visibility:hidden !important; opacity:0 !important; position:absolute !important; z-index:-9999 !important; height:0; width:0; overflow:hidden;">'; $honeypots_raw = $this->config->get_string('honeypot_urls', ''); $honeypots = array_filter(array_map('trim', explode("\n", $honeypots_raw))); if (empty($honeypots)) { $honeypots = [ '/wp-admin.bak', '/wp-login.php.bak', '/license.txt', '/readme.html', '/xmlrpc.php.bak', '/admin.php', '/administrator/index.php', '/wp-content/uploads/.htaccess.bak', '/.env', '/config.php.bak' ]; } foreach ($honeypots as $index => $honeypot) { echo '<a href="' . esc_url(home_url($honeypot)) . '" id="' . esc_attr('botfend-honeypot-' . absint($index)) . '" data-bot-trap="true"></a>'; } echo '</div>'; // Enqueue combined JS with honeypot data $this->enqueue_challenges_js(); wp_add_inline_script( 'botfend-challenges-js', 'var botfendHoneypot = ' . wp_json_encode([ 'nonce' => wp_create_nonce('botfend_honeypot'), 'beaconUrl' => home_url('/?botfend_honeypot=1'), ]) . ';', 'before' ); } public function check_honeypot_hit() { if (!isset($_GET['botfend_honeypot'])) { return; } $nonce = isset($_GET['nonce']) ? sanitize_text_field(wp_unslash($_GET['nonce'])) : ''; $honeypot_value = isset($_GET['botfend_honeypot']) ? sanitize_text_field(wp_unslash($_GET['botfend_honeypot'])) : ''; if (!preg_match('/^[a-zA-Z0-9_-]+$/', $honeypot_value)) { $this->handle_malformed_honeypot(); return; } if (empty($nonce)) { $this->handle_dumb_bot_honeypot($honeypot_value); return; } if (!wp_verify_nonce($nonce, 'botfend_honeypot')) { $this->handle_honeypot_csrf_attack(); return; } $honeypot_id = isset($_GET['honeypot_id']) ? sanitize_text_field(wp_unslash($_GET['honeypot_id'])) : 'unknown'; $this->process_valid_honeypot_hit($honeypot_value, $honeypot_id); } private function handle_dumb_bot_honeypot($honeypot_value) { $ip = $this->utilities->get_client_ip(); $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; $country = $this->utilities->get_country($ip); $this->database->log_request( sanitize_text_field($ip), 3, 'honeypot_dumb_bot', [esc_html__('Dumb bot hit honeypot (no JS)', 'botfend-anti-bot-firewall')], sanitize_text_field($user_agent), false, sanitize_text_field($country) ); $hits_key = 'botfend_honeypot_hits_' . md5(sanitize_text_field($ip)); $hits = absint(get_transient($hits_key) ?: 0); $hits++; set_transient($hits_key, $hits, HOUR_IN_SECONDS); if ($hits >= 3) { $block_duration = $this->calculate_honeypot_block_duration($hits); $block_reason = sprintf( esc_html__('Multiple honeypot hits without JS (%d hits)', 'botfend-anti-bot-firewall'), $hits ); $this->database->block_ip( sanitize_text_field($ip), 'honeypot_repeat_bot', $block_reason, 7, $block_duration, sanitize_text_field($country), sanitize_text_field($user_agent) ); } $this->send_honeypot_security_headers(); status_header(404); nocache_headers(); exit; } private function handle_honeypot_csrf_attack() { $ip = $this->utilities->get_client_ip(); $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; $country = $this->utilities->get_country($ip); $referer = isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : ''; $this->database->log_request( sanitize_text_field($ip), 10, 'honeypot_csrf_attack', [sprintf( esc_html__('CSRF attack on security honeypot from IP: %s', 'botfend-anti-bot-firewall'), esc_html($ip) )], sanitize_text_field($user_agent), false, sanitize_text_field($country), sanitize_text_field($referer) ); $attack_key = 'botfend_csrf_attack_' . md5(sanitize_text_field($ip)); $attack_count = absint(get_transient($attack_key) ?: 0); $attack_count++; set_transient($attack_key, $attack_count, HOUR_IN_SECONDS); $block_duration = $this->calculate_honeypot_block_duration($attack_count); $block_reason = sprintf( esc_html__('CSRF attack on security honeypot (attempt %d)', 'botfend-anti-bot-firewall'), $attack_count ); $this->database->block_ip( sanitize_text_field($ip), 'honeypot_csrf', $block_reason, 10, $block_duration, sanitize_text_field($country), sanitize_text_field($user_agent) ); $this->send_honeypot_security_headers(); status_header(404); nocache_headers(); exit; } private function handle_malformed_honeypot() { $ip = $this->utilities->get_client_ip(); $this->database->log_request( sanitize_text_field($ip), 6, 'malformed_honeypot', [esc_html__('Malformed honeypot value detected', 'botfend-anti-bot-firewall')], isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '', false, $this->utilities->get_country($ip) ); status_header(404); nocache_headers(); exit; } private function process_valid_honeypot_hit($honeypot_value, $honeypot_id) { $ip = $this->utilities->get_client_ip(); $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; $country = $this->utilities->get_country($ip); if ($this->is_ip_whitelisted($ip)) { $this->database->log_request( sanitize_text_field($ip), 0, 'whitelist_honeypot', [sprintf( esc_html__('Whitelisted IP hit honeypot: %s', 'botfend-anti-bot-firewall'), esc_html($honeypot_id) )], sanitize_text_field($user_agent), false, sanitize_text_field($country) ); wp_safe_redirect(esc_url(home_url())); exit; } $this->database->log_request( sanitize_text_field($ip), 9, 'honeypot_triggered', [sprintf( esc_html__('Honeypot triggered: %1$s (value: %2$s)', 'botfend-anti-bot-firewall'), esc_html($honeypot_id), esc_html($honeypot_value) )], sanitize_text_field($user_agent), true, sanitize_text_field($country) ); $hits_key = 'botfend_honeypot_hits_' . md5(sanitize_text_field($ip)); $hits = absint(get_transient($hits_key) ?: 0); $hits++; set_transient($hits_key, $hits, self::HONEYPOT_WINDOW); if ($hits >= 1) { $block_duration = $this->calculate_honeypot_block_duration($hits); $block_reason = sprintf( esc_html__('Bot detected via honeypot trap (%d hits)', 'botfend-anti-bot-firewall'), $hits ); $this->database->block_ip( sanitize_text_field($ip), 'honeypot_bot', $block_reason, 9, $block_duration, sanitize_text_field($country), sanitize_text_field($user_agent) ); } $this->send_honeypot_security_headers(); status_header(404); nocache_headers(); exit; } private function calculate_honeypot_block_duration($attack_count) { switch (true) { case $attack_count >= 10: return 720; case $attack_count >= 5: return 336; case $attack_count >= 3: return 168; case $attack_count >= 2: return 72; default: return $this->config->get_int('honeypot_block_duration', 24); } } private function send_honeypot_security_headers() { if (!headers_sent()) { header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: SAMEORIGIN'); header('Referrer-Policy: strict-origin-when-cross-origin'); if (is_ssl()) { header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); } } } public function should_bypass_challenges($ip, $request_uri = '') { if (empty($request_uri)) { $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''; } return $this->is_ip_whitelisted($ip) || $this->is_path_whitelisted($request_uri); } public function get_whitelisted_paths() { return $this->whitelisted_paths; } public function get_whitelisted_ips() { return $this->whitelisted_ips; } public function clear_challenge_data($ip) { global $wpdb; $hash = md5($ip); if (!wp_using_ext_object_cache()) { // SAFE: Only run if no object cache is present to prevent ghost transients $keys = $wpdb->get_col($wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", '_transient_botfend_verify_attempts_' . $wpdb->esc_like($hash) . '%', '_transient_timeout_botfend_verify_attempts_' . $wpdb->esc_like($hash) . '%' )); if (!empty($keys)) { foreach ($keys as $key) { $transient_name = str_replace(['_transient_timeout_', '_transient_'], '', $key); delete_transient($transient_name); } } } $challenge_key = 'botfend_challenge_count_' . $hash; delete_transient($challenge_key); $verified_key = 'botfend_verified_' . $hash; delete_transient($verified_key); } }