plugin = $plugin_instance; } /** * Verifies a JS challenge response if submitted. * Executes before block rules to process * a challenge response and set the verification cookie. */ public function verify_submission() { // phpcs:ignore WordPress.Security.NonceVerification.Missing if (!isset($_POST['_advaipbl_js_token'])) { return; } if (!empty($this->plugin->request_is_asn_whitelisted) || $this->plugin->is_whitelisted($this->plugin->get_client_ip()) || !empty($this->plugin->is_advanced_rule_allowed)) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing $challenge_type = sanitize_key($_POST['_advaipbl_challenge_type'] ?? 'signature'); $global_duration_hours = (int)($this->plugin->options['global_challenge_cookie_duration'] ?? 4); $cookie_duration = ($global_duration_hours > 0) ? $global_duration_hours * HOUR_IN_SECONDS : 0; if ($challenge_type === 'geo_challenge') { $duration_hours = (int)($this->plugin->options['geo_challenge_cookie_duration'] ?? 24); $cookie_duration = ($duration_hours > 0) ? $duration_hours * HOUR_IN_SECONDS : 0; } $this->verify_challenge('advaipbl_js_verified', $cookie_duration); } /** * Validates a cryptographic VIP Pass (HMAC Cookie) * @param string $ip * @return bool */ public function is_vip_pass_valid($ip = null) { if (!$ip) { $ip = $this->plugin->get_client_ip(); } $cookie_name = 'advaipbl_js_verified'; if (!isset($_COOKIE[$cookie_name])) { return false; } $cookie_value = sanitize_text_field(wp_unslash($_COOKIE[$cookie_name])); if ($cookie_value === '1') { return false; } $decoded = base64_decode($cookie_value); if ($decoded === false || strpos($decoded, '|') === false) { return false; } list($expiration, $signature) = explode('|', $decoded, 2); if ((int)$expiration !== 0 && time() > (int)$expiration) { return false; } $salt = function_exists('wp_salt') ? wp_salt('auth') : (defined('AUTH_SALT') ? AUTH_SALT : 'fallback_salt_advaipbl'); $modifier = get_option('advaipbl_vip_salt_modifier', ''); $expected_signature = hash_hmac('sha256', $ip . $expiration, $salt . $modifier); return hash_equals($expected_signature, $signature); } /** * Computes a deterministic 32-bit FNV-1a hash matching the client JS implementation. * * @param string $input Input string. * @return int Unsigned 32-bit integer hash. */ private function compute_fnv32($input) { $h = 0x811c9dc5; $len = strlen($input); for ($i = 0; $i < $len; $i++) { $h ^= ord($input[$i]); $h = ($h * 0x01000193) & 0xffffffff; } return $h; } /** * Verifies the challenge token and sets the cookie if correct. * * @param string $cookie_name Name of the cookie to set. * @param int $cookie_duration Cookie duration in seconds. */ public function verify_challenge($cookie_name, $cookie_duration) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $token = sanitize_text_field(wp_unslash($_POST['_advaipbl_js_token'] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Missing $response_raw = sanitize_text_field(wp_unslash($_POST['_advaipbl_js_response'] ?? '')); $ip = $this->plugin->get_client_ip(); $is_valid = false; if (!empty($token) && !empty($response_raw) && strpos($response_raw, ':') !== false) { $decoded = base64_decode(strtr($token, '-_', '+/')); if ($decoded !== false && strpos($decoded, '::') !== false) { list($payload, $signature) = explode('::', $decoded, 2); $salt = function_exists('wp_salt') ? wp_salt('auth') : (defined('AUTH_SALT') ? AUTH_SALT : 'fallback_salt_advaipbl'); $expected_signature = hash_hmac('sha256', $payload . '|' . $ip, $salt); if (hash_equals($expected_signature, $signature)) { $parts = explode('|', $payload); if (count($parts) === 5) { $issued_at = (int) $parts[0]; $expiration = (int) $parts[1]; $seed = $parts[2]; $token_mode = $parts[3]; $nonce_id = preg_replace('/[^a-f0-9]/', '', $parts[4]); $now = time(); if ($now <= $expiration && ($now - $issued_at) >= 1 && !empty($nonce_id) && !get_transient('advaipbl_jsc_used_' . $nonce_id)) { list($pow_nonce_str, $submitted_proof) = explode(':', $response_raw, 2); $pow_nonce = (int) $pow_nonce_str; if ($pow_nonce > 0 && strlen($submitted_proof) === 8) { $pow_hash = $this->compute_fnv32($seed . ':' . $pow_nonce); if (($pow_hash & 0x0fff) === 0x0a1b) { if ($token_mode === 'managed') { // phpcs:ignore WordPress.Security.NonceVerification.Missing if (isset($_POST['human_check'])) { $expected_proof = sprintf('%08x', $this->compute_fnv32($seed . ':' . $pow_nonce . ':managed:1:' . $pow_hash)); if (hash_equals($expected_proof, strtolower($submitted_proof))) { $is_valid = true; } } } else { $expected_proof = sprintf('%08x', $this->compute_fnv32($seed . ':' . $pow_nonce . ':auto:' . $pow_hash)); if (hash_equals($expected_proof, strtolower($submitted_proof))) { $is_valid = true; } } } } if ($is_valid) { set_transient('advaipbl_jsc_used_' . $nonce_id, 1, 300); } } } } } } if ($is_valid) { if (isset($this->plugin->challenge_metrics)) { $this->plugin->challenge_metrics->increment('passed'); } set_transient('advaipbl_grace_pass_' . md5($ip), true, 15); $this->set_vip_pass_cookie($cookie_duration, $ip); } else { if (isset($this->plugin->challenge_metrics)) { $this->plugin->challenge_metrics->increment('failed'); } $this->plugin->log_event("JS Challenge verification failed (IP: {$ip}).", 'warning', [ 'action' => 'js_challenge_failed', 'ip' => $ip, 'response' => $response_raw, ]); } if (! defined('DONOTCACHEPAGE')) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound define('DONOTCACHEPAGE', true); } header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); wp_die( esc_html__('Verification failed. Please ensure JavaScript and cookies are enabled in your browser.', 'advanced-ip-blocker'), esc_html__('Access Denied', 'advanced-ip-blocker'), ['response' => 403] ); } /** * Generates and sets the VIP pass cookie. * @param int $cookie_duration * @param string|null $ip */ public function set_vip_pass_cookie($cookie_duration, $ip = null) { if (!$ip) { $ip = $this->plugin->get_client_ip(); } $cookie_name = 'advaipbl_js_verified'; $expiration = ($cookie_duration > 0) ? time() + $cookie_duration : 0; $request_uri = esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'] ?? '')); $salt = function_exists('wp_salt') ? wp_salt('auth') : (defined('AUTH_SALT') ? AUTH_SALT : 'fallback_salt_advaipbl'); $modifier = get_option('advaipbl_vip_salt_modifier', ''); $signature = hash_hmac('sha256', $ip . $expiration, $salt . $modifier); $cookie_value = base64_encode($expiration . '|' . $signature); if (defined('ADVAIPBL_EDGE_MODE') && ADVAIPBL_EDGE_MODE) { if (ob_get_level()) { ob_end_clean(); } $cookie_options = [ 'expires' => $expiration, 'path' => '/', 'domain' => defined('COOKIE_DOMAIN') && COOKIE_DOMAIN ? COOKIE_DOMAIN : '', 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax' ]; setcookie($cookie_name, $cookie_value, $cookie_options); wp_safe_redirect($request_uri, 303); exit; } else { setcookie($cookie_name, $cookie_value, $expiration, '/', defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : '', is_ssl(), true); wp_safe_redirect($request_uri, 303); exit; } } public function serve_challenge($challenge_type, $challenge_mode = 'managed') { if (isset($this->plugin->challenge_metrics)) { $this->plugin->challenge_metrics->increment('served'); } $global_engine = $this->plugin->options['default_challenge_engine'] ?? 'js_managed'; if ($challenge_mode === 'default' || $challenge_mode === 'managed' || $challenge_mode === 'automatic') { $challenge_mode = $global_engine; } $engine = 'js_challenge'; $final_mode = 'managed'; if ($challenge_mode === 'js_managed') { $engine = 'js_challenge'; $final_mode = 'managed'; } elseif ($challenge_mode === 'js_automatic') { $engine = 'js_challenge'; $final_mode = 'automatic'; } elseif ($challenge_mode === 'turnstile') { $engine = 'turnstile'; $final_mode = 'managed'; } elseif ($challenge_mode === 'hcaptcha') { $engine = 'hcaptcha'; $final_mode = 'managed'; } if ($engine === 'turnstile' || $engine === 'hcaptcha') { $has_keys = false; if ($engine === 'turnstile') { $site_key = $this->plugin->options['turnstile_site_key'] ?? ''; $secret_key = $this->plugin->options['turnstile_secret_key'] ?? ''; $has_keys = !empty($site_key) && !empty($secret_key); } else { $site_key = $this->plugin->options['hcaptcha_site_key'] ?? ''; $secret_key = $this->plugin->options['hcaptcha_secret_key'] ?? ''; $has_keys = !empty($site_key) && !empty($secret_key); } if (!$has_keys) { $this->plugin->log_event( sprintf("Missing API keys for %s. Falling back to JS Automatic Challenge to prevent lockout.", ucfirst($engine)), 'warning', ['action' => 'captcha_fallback', 'module' => $challenge_type] ); $engine = 'js_challenge'; $final_mode = 'automatic'; } } if (($engine === 'turnstile' || $engine === 'hcaptcha') && isset($this->plugin->captcha_manager)) { $this->plugin->captcha_manager->serve_challenge($challenge_type, $engine, $final_mode); exit; } $challenge_mode = $final_mode; if (!defined('DONOTCACHEPAGE')) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Required constant name for caching plugins define('DONOTCACHEPAGE', true); } if (headers_sent()) { return; } header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0'); header('Pragma: no-cache'); header('Expires: Thu, 01 Jan 1970 00:00:01 GMT'); header('X-LiteSpeed-Cache-Control: no-cache'); header('Cloudflare-CDN-Cache-Control: no-store'); header('CDN-Cache-Control: no-store'); header('Surrogate-Control: no-store'); header('X-Accel-Expires: 0'); header('X-Cache-Enabled: False'); header('X-Frame-Options: DENY'); header('X-Content-Type-Options: nosniff'); header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline';"); $ip = $this->plugin->get_client_ip(); $issued_at = time(); $expiration = $issued_at + 300; $seed = bin2hex(random_bytes(8)); $nonce_id = bin2hex(random_bytes(6)); $seed_p1 = substr($seed, 0, 8); $seed_p2_rev = strrev(substr($seed, 8)); $payload = $issued_at . '|' . $expiration . '|' . $seed . '|' . $challenge_mode . '|' . $nonce_id; $salt = function_exists('wp_salt') ? wp_salt('auth') : (defined('AUTH_SALT') ? AUTH_SALT : 'fallback_salt_advaipbl'); $signature = hash_hmac('sha256', $payload . '|' . $ip, $salt); $token_raw = $payload . '::' . $signature; $token = strtr(base64_encode($token_raw), '+/', '-_'); status_header(503); header('Content-Type: text/html; charset=utf-8'); header('Retry-After: 10'); $protocol = is_ssl() ? 'https://' : 'http://'; $host = sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'] ?? '')); $uri = esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'] ?? '/')); $action_url = esc_url($protocol . $host . $uri); $site_host = wp_parse_url(home_url(), PHP_URL_HOST); $site_msg = esc_html__('needs to review the security of your connection before proceeding.', 'advanced-ip-blocker'); $site_title = get_bloginfo('name', 'display'); $page_title = esc_html__('Verifying your connection...', 'advanced-ip-blocker'); $main_heading = esc_html__('Security Check Required', 'advanced-ip-blocker'); $timer_text = esc_html__('Time remaining: ', 'advanced-ip-blocker'); $noscript_text = esc_html__('Please enable JavaScript to continue.', 'advanced-ip-blocker'); $button_text = esc_html__('Verify and Continue', 'advanced-ip-blocker'); $checkbox_label = esc_html__('I am not a robot.', 'advanced-ip-blocker'); $expired_heading = esc_html__('Session Expired', 'advanced-ip-blocker'); $expired_message = esc_html__('The security challenge has expired. Click the button below to get a new challenge.', 'advanced-ip-blocker'); $reload_button_text = esc_html__('Start New Challenge', 'advanced-ip-blocker'); $pow_js_core = " function fnv32(str) { let h = 0x811c9dc5; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; } return h >>> 0; } function toHex8(n) { let s = (n >>> 0).toString(16); while (s.length < 8) { s = '0' + s; } return s; } function solvePoW() { const isBot = !!(navigator.webdriver || window.callPhantom || window._phantom || window.__nightmare || (window.outerWidth === 0 && window.outerHeight === 0)); const s1 = document.getElementById('challenge_form').getAttribute('data-challenge-s') || ''; const s2 = '{$seed_p2_rev}'.split('').reverse().join(''); const seed = isBot ? 'bot' : (s1 + s2); let nonce = 0; let hash = 0; while (nonce < 5000000) { nonce++; hash = fnv32(seed + ':' + nonce); if ((hash & 0x0fff) === 0x0a1b) { break; } } return { seed: seed, nonce: nonce, hash: hash }; } "; if ($challenge_mode === 'automatic') { $main_text = esc_html__('Please wait while we verify your connection. This process is automatic and protects the website from automated attacks.', 'advanced-ip-blocker'); $js_script = ""; $form_content = '
'; } else { $main_text = esc_html__('To proceed, please prove you are human. This verification protects the website from automated attacks.', 'advanced-ip-blocker'); $js_script = ""; $form_content = ' '; } $html = '' . esc_html($site_host) . ' ' . $site_msg . '
' . $main_text . '
' . $expired_message . '
' . esc_html__('Performance & security by', 'advanced-ip-blocker') . ' Advanced IP Blocker