²ιΏ΄/±ΰΌ ΄ϊΒλ
ΔΪΘέ
<?php /** * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ * LANDING SYSTEM - ADVANCED DETECTION (Phase 2) * Enhanced scanner detection with timing, rate tracking, and caching * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ class AdvancedDetection { private static $cacheFile = null; private static $rateFile = null; /** * Initialize cache file paths */ private static function initPaths() { if (self::$cacheFile === null) { self::$cacheFile = DATA_PATH . '/ip_cache.json'; self::$rateFile = DATA_PATH . '/rate_tracking.json'; } } // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // IP REPUTATION CACHE // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ /** * Get cached detection result for IP * * @param string $ip IP address * @return array|null Cached result or null if not found/expired */ public static function getCachedIpResult($ip) { if (!ENABLE_IP_REPUTATION_CACHE) return null; self::initPaths(); if (!file_exists(self::$cacheFile)) return null; $cache = json_decode(file_get_contents(self::$cacheFile), true); if (!is_array($cache)) return null; $ipHash = md5($ip); if (!isset($cache[$ipHash])) return null; $entry = $cache[$ipHash]; // Check expiration if (time() - $entry['timestamp'] > IP_CACHE_TTL_SECONDS) { return null; } return $entry['result']; } /** * Cache detection result for IP * * @param string $ip IP address * @param array $result Detection result */ public static function cacheIpResult($ip, $result) { if (!ENABLE_IP_REPUTATION_CACHE) return; self::initPaths(); $cache = []; if (file_exists(self::$cacheFile)) { $cache = json_decode(file_get_contents(self::$cacheFile), true) ?? []; } // Clean expired entries (max 1000 entries) if (count($cache) > 1000) { $cache = self::cleanExpiredCache($cache); } $ipHash = md5($ip); $cache[$ipHash] = [ 'timestamp' => time(), 'result' => [ 'is_scanner' => $result['is_scanner'], 'confidence' => $result['confidence'], 'vendor' => $result['vendor'], ], ]; file_put_contents(self::$cacheFile, json_encode($cache), LOCK_EX); } /** * Clean expired cache entries */ private static function cleanExpiredCache($cache) { $now = time(); $cleaned = []; foreach ($cache as $hash => $entry) { if ($now - $entry['timestamp'] <= IP_CACHE_TTL_SECONDS) { $cleaned[$hash] = $entry; } } return $cleaned; } // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // RATE TRACKING // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ /** * Track request and check rate limits * * @param string $ip IP address * @return array ['allowed' => bool, 'reason' => string|null, 'score' => int] */ public static function checkRateLimit($ip) { if (!ENABLE_RATE_TRACKING) { return ['allowed' => true, 'reason' => null, 'score' => 0]; } self::initPaths(); $tracking = []; if (file_exists(self::$rateFile)) { $tracking = json_decode(file_get_contents(self::$rateFile), true) ?? []; } $ipHash = md5($ip); $now = time(); $minuteAgo = $now - 60; $hourAgo = $now - 3600; // Initialize or get existing tracking if (!isset($tracking[$ipHash])) { $tracking[$ipHash] = ['requests' => []]; } // Clean old entries $tracking[$ipHash]['requests'] = array_filter( $tracking[$ipHash]['requests'], function($ts) use ($hourAgo) { return $ts > $hourAgo; } ); // Count requests $requestsLastMinute = count(array_filter( $tracking[$ipHash]['requests'], function($ts) use ($minuteAgo) { return $ts > $minuteAgo; } )); $requestsLastHour = count($tracking[$ipHash]['requests']); // Add current request $tracking[$ipHash]['requests'][] = $now; // Save tracking file_put_contents(self::$rateFile, json_encode($tracking), LOCK_EX); // Calculate score and check limits $score = 0; $reason = null; if ($requestsLastMinute >= MAX_REQUESTS_PER_MINUTE) { $score = 80; $reason = 'rate_limit_minute'; } elseif ($requestsLastHour >= MAX_REQUESTS_PER_HOUR) { $score = 60; $reason = 'rate_limit_hour'; } elseif ($requestsLastMinute > MAX_REQUESTS_PER_MINUTE / 2) { $score = 30; $reason = 'high_frequency'; } return [ 'allowed' => $score < 60, 'reason' => $reason, 'score' => $score, 'requests_minute' => $requestsLastMinute, 'requests_hour' => $requestsLastHour, ]; } // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // TIMING ANALYSIS // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ /** * Analyze request timing for bot indicators * * @return array ['score' => int, 'reasons' => array] */ public static function analyzeTimingSignals() { if (!ENABLE_TIMING_DETECTION) { return ['score' => 0, 'reasons' => []]; } $score = 0; $reasons = []; // Check request start time (from session) if (session_status() === PHP_SESSION_ACTIVE) { $sessionStart = $_SESSION['request_start'] ?? null; if ($sessionStart === null) { $_SESSION['request_start'] = microtime(true); } else { $elapsed = (microtime(true) - $sessionStart) * 1000; // ms // Suspiciously fast page interaction if ($elapsed < MIN_HUMAN_INTERACTION_MS) { $score += 40; $reasons[] = 'fast_interaction'; } } } // Check if request has timing header (from JS) $clientTiming = $_SERVER['HTTP_X_CLIENT_TIMING'] ?? null; if ($clientTiming !== null) { $timing = intval($clientTiming); if ($timing > 0 && $timing < MIN_PAGE_LOAD_TIME_MS) { $score += 30; $reasons[] = 'fast_page_load'; } } return ['score' => $score, 'reasons' => $reasons]; } // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // ADVANCED HEADER ANALYSIS // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ /** * Deep analysis of request headers * * @return array ['score' => int, 'reasons' => array, 'vendor' => string|null] */ public static function analyzeHeadersAdvanced() { $score = 0; $reasons = []; $vendor = null; // Get all headers $headers = []; foreach ($_SERVER as $key => $value) { if (strpos($key, 'HTTP_') === 0) { $header = str_replace('HTTP_', '', $key); $headers[$header] = $value; } } // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // Check for scanner-specific headers // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ $scannerHeaders = [ // Proofpoint 'X_PP_' => ['vendor' => 'proofpoint', 'score' => 95], 'X_PROOFPOINT' => ['vendor' => 'proofpoint', 'score' => 95], // Mimecast 'X_MIMECAST' => ['vendor' => 'mimecast', 'score' => 95], 'X_MC_' => ['vendor' => 'mimecast', 'score' => 90], // Barracuda 'X_BARRACUDA' => ['vendor' => 'barracuda', 'score' => 95], 'X_ASG_' => ['vendor' => 'barracuda', 'score' => 90], // Microsoft 'X_MS_EXCHANGE' => ['vendor' => 'microsoft', 'score' => 80], 'X_MICROSOFT' => ['vendor' => 'microsoft', 'score' => 85], 'X_FOREFRONT' => ['vendor' => 'microsoft', 'score' => 90], // Generic security 'X_VIRUS_SCANNED' => ['vendor' => 'antivirus', 'score' => 70], 'X_SPAM_STATUS' => ['vendor' => 'spam_filter', 'score' => 70], 'X_SPAM_SCORE' => ['vendor' => 'spam_filter', 'score' => 70], ]; foreach ($headers as $header => $value) { foreach ($scannerHeaders as $pattern => $info) { if (strpos($header, $pattern) === 0) { $score = max($score, $info['score']); $vendor = $info['vendor']; $reasons[] = 'scanner_header_' . strtolower($pattern); break; } } } // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // Check header consistency (browsers have consistent patterns) // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // Real browsers send these in specific order/format $ua = $headers['USER_AGENT'] ?? ''; $accept = $headers['ACCEPT'] ?? ''; $acceptLang = $headers['ACCEPT_LANGUAGE'] ?? ''; $acceptEnc = $headers['ACCEPT_ENCODING'] ?? ''; // Chrome-specific checks if (stripos($ua, 'Chrome') !== false && stripos($ua, 'Safari') !== false) { // Real Chrome sends sec-ch-ua headers if (!isset($headers['SEC_CH_UA']) && !isset($headers['SEC_CH_UA_MOBILE'])) { // Modern Chrome (89+) should have these if (preg_match('/Chrome\/(\d+)/', $ua, $m) && intval($m[1]) >= 89) { $score += 25; $reasons[] = 'missing_sec_ch_ua'; } } // Real Chrome sends sec-fetch headers if (!isset($headers['SEC_FETCH_SITE']) && !isset($headers['SEC_FETCH_MODE'])) { $score += 20; $reasons[] = 'missing_sec_fetch'; } } // Firefox-specific checks if (stripos($ua, 'Firefox') !== false && stripos($ua, 'Chrome') === false) { // Firefox doesn't send sec-ch-ua but sends DNT often if (isset($headers['SEC_CH_UA'])) { $score += 40; $reasons[] = 'firefox_with_chrome_headers'; } } // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // Check for automation tool headers // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ $automationHeaders = [ 'X_AUTOMATION', 'X_PUPPETEER', 'X_SELENIUM', 'X_PLAYWRIGHT', 'X_CYPRESS', 'X_TEST', 'X_DEBUG', ]; foreach ($automationHeaders as $autoHeader) { if (isset($headers[$autoHeader])) { $score += 90; $vendor = 'automation_tool'; $reasons[] = 'automation_header'; break; } } // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // Check Accept-Language format // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ if (!empty($acceptLang)) { // Real browsers send quality values if (strpos($acceptLang, ';q=') === false && strpos($acceptLang, ',') !== false) { $score += 15; $reasons[] = 'accept_lang_no_quality'; } } // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // Check connection header // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ $connection = $headers['CONNECTION'] ?? ''; if (strtolower($connection) === 'close') { // Bots often use Connection: close $score += 10; $reasons[] = 'connection_close'; } return [ 'score' => $score, 'reasons' => $reasons, 'vendor' => $vendor, ]; } // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // HEADLESS BROWSER DETECTION (Server-side hints) // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ /** * Check for headless browser indicators in headers/UA * * @return array ['score' => int, 'reasons' => array] */ public static function checkHeadlessIndicators() { $score = 0; $reasons = []; $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; $uaLower = strtolower($ua); // Direct headless indicators $headlessPatterns = [ 'headlesschrome' => 95, 'headless' => 80, 'phantomjs' => 95, 'phantom' => 70, 'puppeteer' => 95, 'playwright' => 95, 'selenium' => 90, 'webdriver' => 90, 'chromedriver' => 90, 'geckodriver' => 90, ]; foreach ($headlessPatterns as $pattern => $patternScore) { if (strpos($uaLower, $pattern) !== false) { $score = max($score, $patternScore); $reasons[] = 'headless_' . $pattern; } } // Check for missing typical browser features // (These would need JS verification, but we check headers here) // Suspicious UA patterns if (preg_match('/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/\d+ \(KHTML, like Gecko\)$/', $ua)) { // UA without browser version (common in basic bots) $score += 40; $reasons[] = 'incomplete_ua'; } // Chrome UA without version numbers if (stripos($ua, 'Chrome') !== false && !preg_match('/Chrome\/[\d.]+/', $ua)) { $score += 50; $reasons[] = 'chrome_no_version'; } return ['score' => $score, 'reasons' => $reasons]; } // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // COMBINED ADVANCED DETECTION // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ /** * Run all advanced detection checks * * @param string $ip Visitor IP * @return array Combined detection result */ public static function runAdvancedChecks($ip) { $totalScore = 0; $allReasons = []; $vendor = null; // Check cache first $cached = self::getCachedIpResult($ip); if ($cached !== null && $cached['is_scanner']) { return [ 'score' => $cached['confidence'], 'reasons' => ['cached_scanner'], 'vendor' => $cached['vendor'], 'from_cache' => true, ]; } // Rate limiting check $rateResult = self::checkRateLimit($ip); if ($rateResult['score'] > 0) { $totalScore += $rateResult['score']; if ($rateResult['reason']) { $allReasons[] = $rateResult['reason']; } } // Timing analysis $timingResult = self::analyzeTimingSignals(); $totalScore += $timingResult['score']; $allReasons = array_merge($allReasons, $timingResult['reasons']); // Advanced header analysis $headerResult = self::analyzeHeadersAdvanced(); $totalScore += $headerResult['score']; $allReasons = array_merge($allReasons, $headerResult['reasons']); if ($headerResult['vendor']) { $vendor = $headerResult['vendor']; } // Headless browser check $headlessResult = self::checkHeadlessIndicators(); $totalScore += $headlessResult['score']; $allReasons = array_merge($allReasons, $headlessResult['reasons']); // Cap score at 100 $totalScore = min($totalScore, 100); return [ 'score' => $totalScore, 'reasons' => array_unique($allReasons), 'vendor' => $vendor, 'from_cache' => false, 'rate_info' => $rateResult, ]; } } /** * Quick function to run advanced checks */ function run_advanced_detection($ip) { return AdvancedDetection::runAdvancedChecks($ip); } /** * Quick function to check rate limit */ function check_rate_limit($ip) { return AdvancedDetection::checkRateLimit($ip); } /** * Quick function to cache IP result */ function cache_ip_result($ip, $result) { AdvancedDetection::cacheIpResult($ip, $result); }