²é¿´/±à¼ ´úÂë
ÄÚÈÝ
<?php /** * Signal Validator - Cross-reference client signals with server headers * Phase 2.6 Implementation */ class SignalValidator { private $score = 0; private $reasons = []; /** * Validate client signals against server headers * * @param array $clientSignals Signals from JavaScript * @param array $serverHeaders Server headers (typically $_SERVER) * @return array Score and reasons */ public function validate($clientSignals, $serverHeaders = null) { if ($serverHeaders === null) { $serverHeaders = $_SERVER; } $this->score = 0; $this->reasons = []; // Cross-reference checks $this->checkPlatformVsUserAgent($clientSignals, $serverHeaders); $this->checkLanguage($clientSignals, $serverHeaders); $this->checkScreenSize($clientSignals); $this->checkTimezone($clientSignals, $serverHeaders); $this->checkWebRTCIPs($clientSignals, $serverHeaders); return [ 'score' => $this->score, 'reasons' => $this->reasons, 'isAnomalous' => $this->score >= 30 ]; } /** * Check if client platform matches User-Agent */ private function checkPlatformVsUserAgent($clientSignals, $serverHeaders) { $crossRef = $clientSignals['crossRef'] ?? []; $clientPlatform = strtolower($crossRef['platform'] ?? ''); $serverUA = strtolower($serverHeaders['HTTP_USER_AGENT'] ?? ''); if (empty($clientPlatform) || empty($serverUA)) { return; } // Windows platform check if (strpos($clientPlatform, 'win') !== false) { if (strpos($serverUA, 'windows') === false && strpos($serverUA, 'win') === false) { $this->score += 40; $this->reasons[] = 'platform_ua_mismatch_win'; } } // Mac platform check if (strpos($clientPlatform, 'mac') !== false) { if (strpos($serverUA, 'mac') === false && strpos($serverUA, 'darwin') === false) { $this->score += 40; $this->reasons[] = 'platform_ua_mismatch_mac'; } } // Linux platform check if (strpos($clientPlatform, 'linux') !== false) { if (strpos($serverUA, 'linux') === false && strpos($serverUA, 'android') === false) { $this->score += 40; $this->reasons[] = 'platform_ua_mismatch_linux'; } } // iPhone platform vs UA if (strpos($clientPlatform, 'iphone') !== false) { if (strpos($serverUA, 'iphone') === false) { $this->score += 40; $this->reasons[] = 'platform_ua_mismatch_iphone'; } } } /** * Check if client language matches Accept-Language header */ private function checkLanguage($clientSignals, $serverHeaders) { $crossRef = $clientSignals['crossRef'] ?? []; $clientLang = $crossRef['language'] ?? ''; $serverLang = $serverHeaders['HTTP_ACCEPT_LANGUAGE'] ?? ''; if (empty($clientLang) || empty($serverLang)) { return; } // Extract primary language code $clientPrimary = strtolower(substr($clientLang, 0, 2)); $serverPrimary = strtolower(substr($serverLang, 0, 2)); // Check if client language appears in Accept-Language if (strpos(strtolower($serverLang), $clientPrimary) === false) { $this->score += 25; $this->reasons[] = 'language_mismatch'; } } /** * Check for suspicious screen sizes (common VM/bot sizes) */ private function checkScreenSize($clientSignals) { $crossRef = $clientSignals['crossRef'] ?? []; $width = $crossRef['screenWidth'] ?? 0; $height = $crossRef['screenHeight'] ?? 0; // Known VM/automation screen sizes $suspiciousSizes = [ [800, 600], // Common VM default [1024, 768], // Common VM default [1, 1], // Headless [0, 0], // Invalid ]; foreach ($suspiciousSizes as $size) { if ($width == $size[0] && $height == $size[1]) { $this->score += 30; $this->reasons[] = 'suspicious_screen_size'; break; } } // Extremely small screens if ($width > 0 && $width < 640) { $this->score += 20; $this->reasons[] = 'very_small_screen'; } } /** * Check timezone consistency */ private function checkTimezone($clientSignals, $serverHeaders) { $crossRef = $clientSignals['crossRef'] ?? []; $clientTimezone = $crossRef['timezone'] ?? ''; $clientOffset = $crossRef['timezoneOffset'] ?? null; // If we have IP geolocation, compare timezone regions // This is a placeholder for IP-based timezone validation // In production, you'd use a GeoIP service // Basic checks if ($clientTimezone === 'UTC' && $clientOffset === 0) { // Default/unset timezone - slightly suspicious $this->score += 10; $this->reasons[] = 'default_timezone'; } // Check timezone vs offset consistency if ($clientOffset !== null && !empty($clientTimezone)) { $expectedOffset = $this->getTimezoneOffset($clientTimezone); if ($expectedOffset !== null && abs($expectedOffset - $clientOffset) > 60) { $this->score += 20; $this->reasons[] = 'timezone_offset_mismatch'; } } } /** * Check WebRTC IPs against HTTP IP */ private function checkWebRTCIPs($clientSignals, $serverHeaders) { $webrtcIPs = $clientSignals['webrtcIPs'] ?? []; $httpIP = $serverHeaders['REMOTE_ADDR'] ?? ''; // Skip if no WebRTC IPs (could be blocked/unavailable) if (empty($webrtcIPs) || !is_array($webrtcIPs)) { return; } // Check if HTTP IP appears in WebRTC IPs $found = false; foreach ($webrtcIPs as $ip) { if ($ip === $httpIP) { $found = true; break; } // Also check for same /24 subnet (for NAT cases) if ($this->sameSubnet($ip, $httpIP, 24)) { $found = true; break; } } if (!$found && count($webrtcIPs) > 0) { // WebRTC reveals different IP - possible VPN/proxy $this->score += 30; $this->reasons[] = 'webrtc_ip_mismatch'; // Check if WebRTC IPs are datacenter IPs foreach ($webrtcIPs as $ip) { if ($this->isDatacenterIP($ip)) { $this->score += 30; $this->reasons[] = 'webrtc_datacenter_ip'; break; } } } } /** * Get expected timezone offset in minutes */ private function getTimezoneOffset($timezone) { try { $tz = new DateTimeZone($timezone); $now = new DateTime('now', $tz); return $tz->getOffset($now) / -60; // Convert to minutes, invert sign } catch (Exception $e) { return null; } } /** * Check if two IPs are in the same subnet */ private function sameSubnet($ip1, $ip2, $bits = 24) { // Only works for IPv4 if (!filter_var($ip1, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) || !filter_var($ip2, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { return false; } $mask = ~((1 << (32 - $bits)) - 1); $ip1Long = ip2long($ip1) & $mask; $ip2Long = ip2long($ip2) & $mask; return $ip1Long === $ip2Long; } /** * Basic check for datacenter IPs * In production, use a proper IP intelligence service */ private function isDatacenterIP($ip) { // Known datacenter/cloud IP ranges (partial list) $datacenterRanges = [ // AWS '3.', '13.', '18.', '34.', '35.', '52.', '54.', '99.', // Google Cloud '34.', '35.', // Azure '13.', '20.', '40.', '51.', '52.', '65.', '104.', // DigitalOcean '104.', '138.', '159.', '167.', // Linode '45.', '66.', '96.', '139.', '172.', '192.', // OVH '51.', '54.', '91.', '92.', '137.', '139.', '145.', '151.', // Hetzner '88.', '94.', '116.', '136.', '138.', '144.', '148.', ]; foreach ($datacenterRanges as $prefix) { if (strpos($ip, $prefix) === 0) { return true; } } return false; } /** * Analyze behavioral signals * * @param array $signals All client signals * @return array Behavioral analysis result */ public function analyzeBehavior($signals) { $score = 0; $reasons = []; $behavior = $signals['behavior'] ?? []; // Mouse analysis $mouse = $behavior['mouse'] ?? []; if (isset($mouse['constantVelocity']) && $mouse['constantVelocity']) { $score += 40; $reasons[] = 'constant_mouse_velocity'; } if (isset($mouse['superhumanSpeed']) && $mouse['superhumanSpeed']) { $score += 50; $reasons[] = 'superhuman_mouse_speed'; } if (isset($mouse['straightLine']) && $mouse['straightLine']) { $score += 50; $reasons[] = 'straight_line_movement'; } if (isset($mouse['noDirectionChanges']) && $mouse['noDirectionChanges']) { $score += 30; $reasons[] = 'no_direction_changes'; } // Visibility analysis $visibility = $behavior['visibility'] ?? []; if (isset($visibility['neverVisible']) && $visibility['neverVisible']) { $score += 60; $reasons[] = 'page_never_visible'; } if (isset($visibility['hiddenInteractions']) && $visibility['hiddenInteractions']) { $score += 80; $reasons[] = 'interactions_while_hidden'; } // Reaction time analysis $reactions = $behavior['reactions'] ?? []; if (isset($reactions['instantFirstInteraction']) && $reactions['instantFirstInteraction']) { $score += 50; $reasons[] = 'instant_first_interaction'; } if (isset($reactions['identicalReactions']) && $reactions['identicalReactions']) { $score += 40; $reasons[] = 'identical_reaction_times'; } // Scroll analysis $scroll = $behavior['scroll'] ?? []; if (isset($scroll['instantScroll']) && $scroll['instantScroll']) { $score += 40; $reasons[] = 'instant_scroll'; } // Click analysis if (isset($signals['instantClick']) && $signals['instantClick']) { $score += 50; $reasons[] = 'instant_click'; } if (isset($signals['zeroClick']) && $signals['zeroClick']) { $score += 80; $reasons[] = 'zero_coordinate_click'; } return [ 'score' => $score, 'reasons' => $reasons, 'isSuspicious' => $score >= 50 ]; } /** * Analyze environment fingerprint signals * * @param array $signals All client signals * @return array Environment analysis result */ public function analyzeEnvironment($signals) { $score = 0; $reasons = []; // Audio fingerprint $audio = $signals['audio'] ?? []; if (isset($audio['available']) && !$audio['available']) { $score += 20; $reasons[] = 'no_audio_context'; } // Fonts $fonts = $signals['fonts'] ?? []; if (isset($fonts['veryFewFonts']) && $fonts['veryFewFonts']) { $score += 40; $reasons[] = 'very_few_fonts'; } elseif (isset($fonts['fewFonts']) && $fonts['fewFonts']) { $score += 25; $reasons[] = 'few_fonts'; } // Media devices $media = $signals['mediaDevices'] ?? []; if (isset($media['noDevices']) && $media['noDevices']) { $score += 20; $reasons[] = 'no_media_devices'; } if (isset($media['noAudioOutput']) && $media['noAudioOutput']) { $score += 25; $reasons[] = 'no_audio_output'; } // Speech API $speech = $signals['speech'] ?? []; if (isset($speech['synthesis']) && !$speech['synthesis']) { $score += 15; $reasons[] = 'no_speech_synthesis'; } return [ 'score' => $score, 'reasons' => $reasons, 'isSuspicious' => $score >= 40 ]; } /** * Get total combined score from all analyses * * @param array $signals All client signals * @return array Combined analysis result */ public function getFullAnalysis($signals) { // Cross-reference analysis $crossRef = $this->validate($signals); // Behavioral analysis $behavior = $this->analyzeBehavior($signals); // Environment analysis $environment = $this->analyzeEnvironment($signals); // Client-side sandbox score $clientScore = $signals['sandboxScore'] ?? 0; // Combined score $totalScore = $clientScore + $crossRef['score'] + $behavior['score'] + $environment['score']; // Collect all reasons $allReasons = array_merge( $crossRef['reasons'], $behavior['reasons'], $environment['reasons'] ); return [ 'totalScore' => $totalScore, 'clientScore' => $clientScore, 'crossRefScore' => $crossRef['score'], 'behaviorScore' => $behavior['score'], 'environmentScore' => $environment['score'], 'reasons' => $allReasons, 'isSandbox' => $totalScore >= 50, 'isHighRisk' => $totalScore >= 100, 'breakdown' => [ 'crossRef' => $crossRef, 'behavior' => $behavior, 'environment' => $environment ] ]; } }