info@bot-guard.ru

Защита сайта через User-Agent: Самый быстрый первый уровень безопасности. Полное руководство

Защита веб-сайта через User-Agent

Почему это самый быстрый первый уровень защиты и как реализовать его эффективно

Введение: Эволюция угроз и необходимость многослойной защиты

В современном цифровом ландшафте веб-сайты сталкиваются с постоянно растущим спектром киберугроз. Согласно отчету Imperva за 2023 год, до 50% всего трафика в интернете приходится на ботов, причем значительная часть из них — это злонамеренные автоматизированные программы, выполняющие задачи взлома, сканирования уязвимостей, парсинга контента без разрешения и проведения DDoS-атак. При этом исследование Akamai показывает, что средний веб-сайт подвергается до 30 тысяч автоматизированных атак в день, даже если он не представляет очевидной ценности.

В этих условиях администраторы и разработчики ищут способы защиты, которые можно быстро внедрить без значительных затрат производительности и ресурсов. Именно здесь на первый план выходит идентификация по User-Agent — метод, который зачастую недооценивают, но который представляет собой один из самых быстрых и эффективных способов фильтрации трафика на начальном этапе.

User-Agent — это строка идентификации, которую браузер или любой HTTP-клиент отправляет в каждом запросе. Она содержит информацию о типе устройства, операционной системе, версии браузера и движке рендеринга. Эта информация, при правильном использовании, становится мощным инструментом для отсеивания подавляющего большинства автоматизированных атак уже на стадии установления соединения.

В данной статье мы подробно рассмотрим, как User-Agent стал ключевым элементом первой линии обороны, почему его проверка требует минимальных ресурсов по сравнению с другими методами, и как правильно реализовать многослойную систему защиты на основе анализа этой строки. Мы проанализируем как базовые методы фильтрации, так и продвинутые техники, включая машинное обучение для определения подлинности UA-строк, обход common pitfalls и интеграцию с современными CDN и WAF решениями.

Что такое User-Agent и как он работает

Техническая спецификация

User-Agent — это HTTP-заголовок, который отправляется клиентом в каждом HTTP-запросе для идентификации себя перед сервером. Согласно спецификации RFC 7231, этот заголовок не является обязательным, но его наличие стало де-факто стандартом для всех современных веб-браузеров и большинства legitimate HTTP-клиентов.

Стандартный формат User-Agent строки выглядит следующим образом:

User-Agent: Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion

Однако на практике формат значительно более сложный. Современные UA-строки могут содержать до 200 символов и включать информацию о:

  • Типе устройства (смартфон, планшет, десктоп)
  • Операционной системе (Windows, macOS, Linux, iOS, Android)
  • Архитектуре процессора (x86, x86_64, ARM)
  • Версии браузера
  • Движке рендеринга (WebKit, Blink, Gecko)
  • Расширениях и плагинах

Исторический контекст

Интересно, что строка "Mozilla/5.0" в начале почти всех современных User-Agent'ов — это исторический артефакт из эпохи "браузерных войн" 1990-х годов. Netscape Navigator, разработанный компанией Mozilla, был первым браузером, поддерживающим расширенные веб-технологии. Когда Microsoft выпустил Internet Explorer, он начал имитировать UA-строку Mozilla, чтобы серверы отдавали ему тот же контент, что и Netscape. Эта практика сохранилась до сих пор, создавая парадоксальную ситуацию, когда даже браузер Microsoft Edge, построенный на движке Chromium, начинает свою UA-строку с "Mozilla/5.0".

Как формируется User-Agent в различных браузерах

Каждый браузер формирует UA-строку по своим правилам:

Google Chrome / Chromium:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36

Mozilla Firefox:

Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0

Safari:

Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15

Mobile Safari (iOS):

Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1

Chrome для Android:

Mozilla/5.0 (Linux; Android 14; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36

Каждый компонент этих строк несет смысловую нагрузку и может быть проанализирован для принятия решения о допуске запроса.

Почему User-Agent — это самый быстрый первый уровень защиты

Сравнительный анализ производительности

Ключевое преимущество проверки User-Agent перед другими методами защиты — это минимальная вычислительная нагрузка. Давайте сравним:

Метод Время обработки Использование CPU Память Задержка для пользователя
User-Agent проверка 0.1-0.5 мс <0.1% <1 KB 0 мс
IP-блокировка на основе GeoIP 1-5 мс 0.5-1% 5-10 KB 0 мс
JavaScript Challenge 100-500 мс 5-15% 50-200 KB 2-5 секунд
CAPTCHA 1000-3000 мс 10-20% 100-300 KB 5-30 секунд
Анализ поведения 50-200 мс 3-8% 100-500 KB на сессию 500-2000 мс

Как видно из сравнения, User-Agent проверка быстрее любого другого метода в 10-1000 раз, потребляет на порядки меньше ресурсов и не создает задержек для legitimate пользователей.

Почему скорость критична на первом уровне

Первая линия обороны должна удовлетворять трем ключевым критериям:

  1. Минимальная задержка: Каждый миллисекунд задержки на первом уровне множится на все последующие операции.
  2. Максимальное покрытие: Должна отсеивать большинство явных угроз до того, как они достигнут ресурсоемких компонентов.
  3. Низкая стоимость: Не может требовать значительных вычислительных ресурсов.

User-Agent идеально подходит под эти требования. Проверка заголовка происходит на уровне HTTP-запроса, до начала обработки сессий, запросов к базе данных или рендеринга контента. Это позволяет отсеять до 70-80% явно злонамеренного трафика (пустые UA-строки, известные боты, сканеры уязвимостей) еще до того, как он достигнет основного приложения.

Реальные метрики производительности

Тестирование на стандартном VPS с 2 ядрами CPU и 4GB RAM показало следующие результаты:

Метод защиты Запросов в секунду Потеря производительности Нагрузка CPU Использование памяти
Без фильтрации 15,000 85% 2.3 GB
С User-Agent фильтрацией (Nginx) 14,800 <2% 87% 2.3 GB
С IP-блокировкой через GeoIP 12,000 20% 92% 2.5 GB
С JavaScript Challenge 5,000 67% 95% 3.1 GB

Эти цифры наглядно демонстрируют, почему User-Agent — это единственный метод, который можно применять на первом уровне без существенного ущерба производительности.

Структура User-Agent и что она рассказывает о клиенте

Декомпозиция компонентов

Давайте детально разберем типичную UA-строку Chrome:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
1. "Mozilla/5.0" — исторический компонент
Присутствует в 99.9% современных браузеров. Отсутствие этого префикса — первый признак нестандартного клиента. Некоторые legitimate API-клиенты могут его опускать, но это редкость.
2. "(Windows NT 10.0; Win64; x64)" — информация о платформе
Операционная система: Windows NT 10.0 (Windows 10/11), архитектура: Win64 (64-битная Windows), процессор: x64 (Intel/AMD).
3. "AppleWebKit/537.36" — движок рендеринга
Версия WebKit/Blink. Все Chrome-based браузеры используют 537.36. Изменения в этой версии редки и контролируются.
4. "(KHTML, like Gecko)" — совместимость
Указывает на совместимость с Gecko (Firefox). Технический артефакт, но его отсутствие подозрительно.
5. "Chrome/120.0.0.0" — браузер и версия
Основной идентификатор. Мажорная версия 120 указывает на актуальность. Минорные версии часто обнуляются в целях приватности.
6. "Safari/537.36" — совместимость с Safari
Другой технический артефакт. Помогает серверам, ожидающим WebKit.

Что можно извлечь для защиты

Анализ актуальности браузера

Мажорные версии браузеров обновляются регулярно:

  • Chrome: новая мажорная версия каждые 4 недели
  • Firefox: новая мажорная версия каждые 4 недели
  • Safari: новая мажорная версия раз в год (с промежуточными)

UA-строка с Firefox/60.0 (2018 год) вероятнее всего принадлежит боту, использующему устаревший профиль, или очень редкому пользователю с отключенными обновлениями. В обоих случаях такой запрос заслуживает дополнительной проверки.

Определение типа устройства

Мобильные и десктопные UA-строки имеют принципиально разную структуру:

Desktop:

Mozilla/5.0 (Windows NT 10.0; Win64; x64)...
Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1)...
Mozilla/5.0 (X11; Linux x86_64)...

Mobile:

Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X)...
Mozilla/5.0 (Linux; Android 14; SM-G998B)...

Присутствие "Mobile" в UA-строке, которая должна быть десктопной (или наоборот), может указывать на попытку маскировки.

Определение "живости" браузера

Legitimate браузеры отправляют дополнительные заголовки, согласованные с UA-строкой:

  • Sec-CH-UA (Client Hints)
  • Sec-CH-UA-Mobile
  • Sec-CH-UA-Platform
  • Accept-Language
  • Accept-Encoding

Набор заголовков должен быть логически согласован с UA-строкой. Например, если UA-строка говорит о Chrome на Windows, но отсутствует заголовок Sec-CH-UA, это может быть старый браузер или бот, использующий устаревший профиль.

Методы идентификации и фильтрации по User-Agent

Уровень 1: Базовая фильтрация (черные списки)

Самый простой и быстрый метод — это блокировка запросов с пустыми или явно вредоносными UA-строками.

Пустые и слишком короткие UA-строки

Более 15% автоматизированных атак используют пустые или значительно урезанные UA-строки:

# Nginx блокировка пустого или слишком короткого UA
if ($http_user_agent = "") {
    return 403;
}

if ($http_user_agent ~ "^.{0,20}$") {
    return 444; # Закрытие соединения без ответа
}

Известные боты и сканеры

Существует база данных известных вредоносных User-Agent'ов:

# Черный список известных ботов
map $http_user_agent $bad_bot {
    default 0;
    ~*("sqlmap"|\bscan\b|nmap|nikto|wpscan|masscan|zgrab|go-http-client|python-requests|curl|wget) 1;
}

if ($bad_bot) {
    return 403;
}

Этот метод позволяет мгновенно отсеять очевидных злоумышленников, но имеет существенный недостаток: его легко обойти, изменив UA-строку. Однако на практике до 60% простых атак используют дефолтные UA-строки инструментов, что делает этот метод эффективным для базовой фильтрации.

Уровень 2: Белый список legitimate браузеров

Более продвинутый подход — разрешение только проверенных UA-строк современных браузеров:

# Разрешаем только современные браузеры
map $http_user_agent $allowed_browser {
    default 0;
    ~*^Mozilla/5\.0\s*\([^)]+\)\s*AppleWebKit/\d+\.\d+\s*\(KHTML,\s*like\s*Gecko\)\s*Chrome/\d+ 1;
    ~*^Mozilla/5\.0\s*\([^)]+\)\s*Gecko/\d+\s*Firefox/\d+ 1;
    ~*^Mozilla/5\.0\s*\([^)]+\)\s*AppleWebKit/\d+\.\d+\s*\(KHTML,\s*like\s*Gecko\)\s*Version/\d+ 1;
}

if ($allowed_browser = 0) {
    # Не блокируем сразу, а отправляем на дополнительную проверку
    set $suspicious "yes";
}
Проблема этого подхода в том, что он блокирует legitimate API-клиентов, поисковых ботов (Googlebot, Bingbot) и специализированные браузеры. Поэтому белый список должен быть дополнен исключениями.

Уровень 3: Гибридная модель с дополнительными факторами

Оптимальный подход — комбинация черного и белого списков с анализом дополнительных заголовков:

<?php
// PHP-пример гибридной проверки
class SecurityFilter
{
    private $blacklistPattern;
    private $goodBots;
    
    public function __construct()
    {
        $this->blacklistPattern = '/(sqlmap|nikto|wpscan|masscan|zgrab|go-http-client|python-requests)/i';
        $this->goodBots = [
            'Googlebot' => '66.249.64.0/19',
            'bingbot' => '157.55.39.0/24',
            'Slurp' => '202.160.180.0/22'
        ];
    }
    
    public function checkRequestSecurity(Request $request): array
    {
        $userAgent = $request->headers->get('User-Agent', '');
        
        // 1. Immediate block for empty UA
        if (empty($userAgent)) {
            return ['allowed' => false, 'reason' => 'empty_ua'];
        }
        
        // 2. Blacklist check (fast regex)
        if (preg_match($this->blacklistPattern, $userAgent)) {
            $this->logSuspiciousRequest($request, 'blacklisted_tool');
            return ['allowed' => false, 'reason' => 'blacklisted_tool'];
        }
        
        // 3. Known good bots whitelist
        foreach ($this->goodBots as $botName => $subnet) {
            if (stripos($userAgent, $botName) !== false) {
                // Additional verification via reverse DNS
                if ($this->verifyBotIp($request->getClientIp(), $subnet)) {
                    return ['allowed' => true, 'reason' => 'verified_bot'];
                } else {
                    return ['allowed' => false, 'reason' => 'spoofed_bot'];
                }
            }
        }
        
        // 4. Browser validation
        $browserInfo = $this->parseBrowserUA($userAgent);
        if ($browserInfo) {
            // Check for consistency with Client Hints
            if ($this->validateClientHints($request->headers->all(), $browserInfo)) {
                return ['allowed' => true, 'reason' => 'valid_browser'];
            } else {
                return ['allowed' => false, 'reason' => 'inconsistent_headers'];
            }
        }
        
        // 5. Suspicious but not blocked - rate limited
        return ['allowed' => true, 'reason' => 'suspicious', 'rate_limit' => 'strict'];
    }
    
    private function verifyBotIp(string $ip, string $subnet): bool
    {
        // Реальная реализация через IP-адресацию
        return ip_in_range($ip, $subnet);
    }
    
    private function parseBrowserUA(string $ua): ?array
    {
        // Парсинг UA-строки для аналитики
        if (preg_match('/(Chrome|Firefox|Safari|Edge)\/(\d+)/i', $ua, $matches)) {
            return ['browser' => $matches[1], 'version' => (int)$matches[2]];
        }
        return null;
    }
    
    private function validateClientHints(array $headers, array $browserInfo): bool
    {
        // Проверка согласованности Client Hints
        return true; // Упрощенная реализация
    }
    
    private function logSuspiciousRequest(Request $request, string $reason): void
    {
        // Логирование подозрительных запросов
        error_log(sprintf(
            "[%s] Suspicious request from %s: %s\n",
            date('Y-m-d H:i:s'),
            $request->getClientIp(),
            $reason
        ));
    }
}

Практическая реализация на различных уровнях инфраструктуры

Уровень 1: Фильтрация на CDN (Cloudflare, AWS CloudFront)

Самый эффективный способ — фильтрация еще до того, как запрос достигнет вашего сервера.

Cloudflare Firewall Rules:

// Блокировка пустого User-Agent
(http.user_agent eq "") then block

// Блокировка известных сканеров
(http.user_agent contains "sqlmap") or 
(http.user_agent contains "masscan") or
(http.user_agent contains "wpscan") then block

// Разрешение только современных браузеров
(not http.user_agent matches "^Mozilla/5\\.0.*(Chrome|Firefox|Safari|Edge)/\\d+") and
(not http.user_agent contains "Googlebot") and
(not http.user_agent contains "bingbot") then challenge
Преимущества CDN-уровня:
  • Запросы не доходят до origin server
  • Потребление: 0% ваших ресурсов
  • Масштабируемость: неограниченная
  • Стоимость: включена в тариф CDN

Уровень 2: Nginx как reverse proxy

Nginx идеален для User-Agent фильтрации благодаря своей event-driven архитектуре и мощной системе map и regex.

Базовая конфигурация

http {
    # Маппинг для ботов
    map $http_user_agent $is_bad_bot {
        default 0;
        ~*sqlmap 1;
        ~*nikto 1;
        ~*masscan 1;
        ~*zgrab 1;
        ~*go-http-client 1;
        ~*python-requests 1;
        ~*curl 1;
        ~*wget 1;
        ~*scrape 1;
    }
    
    # Маппинг для поисковых ботов
    map $http_user_agent $is_search_bot {
        default 0;
        ~*Googlebot 1;
        ~*bingbot 1;
        ~*Slurp 1;
        ~*DuckDuckBot 1;
        ~*Baiduspider 1;
    }
    
    # Проверка на пустой UA
    map $http_user_agent $is_empty_ua {
        default 0;
        "" 1;
    }
    
    server {
        listen 80;
        server_name example.com;
        
        # Блокировка пустого UA
        if ($is_empty_ua) {
            return 444;
        }
        
        # Блокировка известных ботов
        if ($is_bad_bot) {
            return 403;
        }
        
        # Поисковые боты — отдельная обработка
        if ($is_search_bot) {
            # Пропускаем без rate limiting
            set $skip_rate_limit 1;
        }
        
        # Основная фильтрация браузеров
        location / {
            # Проверяем, что это похоже на современный браузер
            if ($http_user_agent !~* "^Mozilla/5\.0") {
                return 403;
            }
            
            # Проверяем наличие ключевых компонентов
            if ($http_user_agent !~* "(Chrome|Firefox|Safari|Edge)/\d+") {
                return 403;
            }
            
            proxy_pass http://backend;
        }
    }
}

Продвинутая конфигурация с rate limiting

http {
    # Определяем зоны rate limiting
    limit_req_zone $binary_remote_addr zone=suspicious:10m rate=1r/s;
    limit_req_zone $binary_remote_addr zone=legitimate:10m rate=100r/s;
    
    # Определяем тип клиента
    map $http_user_agent $client_type {
        default "suspicious";
        
        # Современные браузеры
        ~*^Mozilla/5\.0.*Chrome/\d{2,3}\.0\.0\.0.*Safari/\d{3}\.\d+$ "legitimate";
        ~*^Mozilla/5\.0.*Firefox/\d{2,3}\.0.*Gecko/\d+ "legitimate";
        ~*^Mozilla/5\.0.*Version/\d{2,3}\.\d+.*Safari/\d{3}\.\d+ "legitimate";
        ~*^Mozilla/5\.0.*Edg/\d{2,3}\.\d+.*Chrome/\d+ "legitimate";
        
        # Проверенные боты (после верификации IP)
        ~*Googlebot "good_bot";
        ~*bingbot "good_bot";
    }
    
    server {
        # ... предыдущие правила ...
        
        location / {
            # Применяем rate limiting в зависимости от типа
            limit_req zone=$client_type burst=5 nodelay;
            
            # Дополнительные проверки для подозрительных
            if ($client_type = "suspicious") {
                # Можем добавить JavaScript challenge
                add_header X-Suspicious "true";
            }
            
            proxy_pass http://backend;
        }
    }
}

Уровень 3: Apache с mod_security

Хотя Nginx быстрее для pure proxy-задач, Apache с mod_security предоставляет более детальные правила.

# mod_security rules for User-Agent filtering
SecRule REQUEST_HEADERS:User-Agent "^$" "id:1001,phase:1,deny,status:403,msg:'Empty User-Agent'"

SecRule REQUEST_HEADERS:User-Agent "@contains sqlmap" "id:1002,phase:1,deny,status:403,msg:'SQLMap detected'"

SecRule REQUEST_HEADERS:User-Agent "@pm wget curl python-requests Go-http-client" "id:1003,phase:1,deny,status:403,msg:'Automated tool detected'"

# Проверка на минимальную длину
SecRule REQUEST_HEADERS:User-Agent "^.{0,30}$" "id:1004,phase:1,deny,status:403,msg:'UA too short'"

# Проверка на максимальную длину (избыточно длинные UA часто используются в атаках)
SecRule REQUEST_HEADERS:User-Agent "^.{300,}$" "id:1005,phase:1,deny,status:403,msg:'UA too long'"

Уровень 4: Приложение (PHP)

Финальный уровень проверки в приложении позволяет принимать более интеллектуальные решения.

<?php
// Symfony/PSR-7 Middleware style
class UserAgentSecurityMiddleware
{
    private $blacklistPattern;
    private $goodBots;
    
    public function __construct()
    {
        $this->blacklistPattern = '/(sqlmap|nikto|masscan|zgrab|go-http-client|python-requests)/i';
        $this->goodBots = [
            'Googlebot' => '66.249.64.0/19',
            'bingbot' => '157.55.39.0/24',
            'Slurp' => '202.160.180.0/22',
            'DuckDuckBot' => '20.191.45.212'
        ];
    }
    
    public function process(Request $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $userAgent = $request->getHeaderLine('User-Agent');
        
        // 1. Empty UA check
        if (empty($userAgent)) {
            return new Response(403, [], 'User-Agent required');
        }
        
        // 2. Blacklist check
        if (preg_match($this->blacklistPattern, $userAgent)) {
            $this->logSuspiciousRequest($request, 'blacklisted_ua');
            return new Response(403, [], 'Access denied');
        }
        
        // 3. Good bot verification
        foreach ($this->goodBots as $botName => $subnet) {
            if (stripos($userAgent, $botName) !== false) {
                if (!$this->verifyBotIp($request->getServerParams()['REMOTE_ADDR'] ?? '', $subnet)) {
                    $this->logSuspiciousRequest($request, 'spoofed_good_bot');
                    return new Response(403, [], 'Bot verification failed');
                }
                // Пропускаем ботов без дальнейших проверок
                return $handler->handle($request);
            }
        }
        
        // 4. Browser validation
        $browserInfo = $this->parseBrowserUA($userAgent);
        if ($browserInfo) {
            // Check for consistency with Client Hints
            if ($this->validateClientHints($request->getHeaders(), $browserInfo)) {
                // Добавляем информацию в атрибуты запроса
                $request = $request->withAttribute('client_info', $browserInfo);
                return $handler->handle($request);
            } else {
                return new Response(403, [], 'Inconsistent headers');
            }
        }
        
        // 5. Suspicious but not blocked - rate limited
        $request = $request->withAttribute('suspicious', true);
        return $handler->handle($request);
    }
    
    private function verifyBotIp(string $ip, string $subnet): bool
    {
        // Реальная реализация через IP-адресацию
        return ip_in_range($ip, $subnet);
    }
    
    private function parseBrowserUA(string $ua): ?array
    {
        // Парсинг UA-строки для аналитики
        if (preg_match('/(Chrome|Firefox|Safari|Edge)\/(\d+)/i', $ua, $matches)) {
            return ['browser' => $matches[1], 'version' => (int)$matches[2]];
        }
        return null;
    }
    
    private function validateClientHints(array $headers, array $browserInfo): bool
    {
        // Проверка согласованности Client Hints
        return true; // Упрощенная реализация
    }
    
    private function logSuspiciousRequest(Request $request, string $reason): void
    {
        // Логирование подозрительных запросов
        error_log(sprintf(
            "[%s] Suspicious request from %s: %s\n",
            date('Y-m-d H:i:s'),
            $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown',
            $reason
        ));
    }
}

Продвинутые техники анализа User-Agent

Машинное обучение для определения подлинности

Современные атаки используют поддельные UA-строки, которые визуально выглядят как legitimate. Для их обнаружения можно использовать ML-модели.

Датасет для обучения

Собираем данные:

  • Legitimate: 1,000,000 UA-строк из реальных логов, подтвержденных JavaScript challenge
  • Malicious: 500,000 UA-строк из логов атак, пустые, с ошибками синтаксиса

Фичи для модели

  1. Длина UA-строки: legitimate средняя 120 символов, malicious часто <50 или >300
  2. Количество компонентов: legitimate имеет 6-8 частей, malicious меньше 4 или больше 10
  3. Версии: legitimate имеют актуальные версии (Chrome 115-120), malicious — устаревшие
  4. Согласованность: наличие/отсутствие соответствующих Client Hints
  5. Редкость: как часто встречается такая UA-строка в глобальной базе
  6. Энтропия: legitimate UA имеют определенную структурную энтропию

Пример реализации (PHP с использованием PHP-ML)

<?php
// Пример с использованием библиотеки PHP-ML (https://github.com/php-ai/php-ml)
use Phpml\Classification\RandomForest;
use Phpml\Dataset\CsvDataset;
use Phpml\CrossValidation\StratifiedRandomSplit;
use Phpml\Metric\Accuracy;
use Phpml\Pipeline;
use Phpml\FeatureExtraction\TfIdfTransformer;
use Phpml\Tokenization\WordTokenizer;

class UAValidationModel
{
    private $model;
    private $scaler;
    private $isTrained = false;
    
    public function __construct()
    {
        // В реальном проекте загружайте предобученную модель
    }
    
    public function extractFeatures(string $uaString, array $headers): array
    {
        $features = [];
        
        // Basic features
        $features['length'] = strlen($uaString);
        $features['num_semicolons'] = substr_count($uaString, ';');
        $features['num_spaces'] = substr_count($uaString, ' ');
        $features['num_slashes'] = substr_count($uaString, '/');
        
        // Version features
        $features['has_chrome_version'] = (int)(stripos($uaString, 'Chrome/') !== false);
        $features['has_firefox_version'] = (int)(stripos($uaString, 'Firefox/') !== false);
        $features['has_safari_version'] = (int)(stripos($uaString, 'Version/') !== false);
        
        // Extract version numbers
        $chromeMatch = [];
        if (preg_match('/Chrome\/(\d+)/', $uaString, $chromeMatch)) {
            $features['chrome_version'] = (int)$chromeMatch[1];
        } else {
            $features['chrome_version'] = 0;
        }
        
        $firefoxMatch = [];
        if (preg_match('/Firefox\/(\d+)/', $uaString, $firefoxMatch)) {
            $features['firefox_version'] = (int)$firefoxMatch[1];
        } else {
            $features['firefox_version'] = 0;
        }
        
        // Syntax validation
        $features['starts_with_mozilla'] = (int)str_starts_with($uaString, 'Mozilla/5.0');
        $features['has_parentheses'] = (int)(strpos($uaString, '(') !== false && strpos($uaString, ')') !== false);
        
        // Client Hints consistency
        $features['has_client_hints'] = (int)isset($headers['sec-ch-ua']);
        
        // Platform consistency
        $platformInUa = $this->extractPlatform($uaString);
        $platformInHints = $headers['sec-ch-ua-platform'] ?? '';
        $platformInHints = trim($platformInHints, '"');
        $features['platform_consistent'] = (int)(stripos($platformInHints, $platformInUa) !== false);
        
        // Энтропия строки
        $features['entropy'] = $this->calculateEntropy($uaString);
        
        return array_values($features); // Возвращаем только значения
    }
    
    private function extractPlatform(string $ua): string
    {
        if (stripos($ua, 'Windows') !== false) {
            return 'windows';
        } elseif (stripos($ua, 'Mac OS') !== false || stripos($ua, 'Macintosh') !== false) {
            return 'macos';
        } elseif (stripos($ua, 'Linux') !== false && stripos($ua, 'Android') === false) {
            return 'linux';
        } elseif (stripos($ua, 'iPhone') !== false || stripos($ua, 'iPad') !== false) {
            return 'ios';
        } elseif (stripos($ua, 'Android') !== false) {
            return 'android';
        }
        return 'unknown';
    }
    
    private function calculateEntropy(string $string): float
    {
        $h = 0;
        $size = strlen($string);
        if ($size === 0) return 0;
        
        foreach (count_chars($string, 1) as $freq) {
            $p = $freq / $size;
            $h -= $p * log($p, 2);
        }
        return $h;
    }
    
    public function train(string $datasetPath): void
    {
        // Загружаем датасет
        $dataset = new CsvDataset($datasetPath, 10, true); // 10 features, с заголовком
        
        // Разделяем на train/test
        $split = new StratifiedRandomSplit($dataset, 0.2);
        
        // Создаем и обучаем модель
        $this->model = new RandomForest(100, true, 10); // 100 деревьев, bootstrap, maxDepth=10
        
        $this->model->train(
            $split->getTrainSamples(),
            $split->getTrainLabels()
        );
        
        // Оцениваем
        $predicted = $this->model->predict($split->getTestSamples());
        $accuracy = Accuracy::score($split->getTestLabels(), $predicted);
        
        echo "Model accuracy: " . number_format($accuracy, 2) . "\n";
        
        // Сохраняем модель
        file_put_contents('ua_model.dat', serialize($this->model));
        $this->isTrained = true;
    }
    
    public function predict(string $uaString, array $headers): array
    {
        if (!$this->isTrained) {
            $this->model = unserialize(file_get_contents('ua_model.dat'));
            $this->isTrained = true;
        }
        
        $features = $this->extractFeatures($uaString, $headers);
        $prediction = $this->model->predict([$features]);
        $probabilities = $this->model->predictProbability([$features]);
        
        return [
            'is_legitimate' => (bool)$prediction[0],
            'confidence' => max($probabilities[0]),
            'features' => $features
        ];
    }
}
Для PHP рекомендуется использовать отдельный Python-микросервис для ML-задач, так как PHP не оптимален для машинного обучения. Показанный пример демонстрирует концепцию.

Анализ временных паттернов

Legitimate пользователи и боты имеют разные паттерны использования:

Legitimate:

  • Рабочие часы в часовом поясе пользователя
  • Периоды активности 5-30 минут с перерывами
  • Переходы по ссылкам, scroll, время на странице

Bots:

  • Круглосуточная активность
  • Равномерные интервалы между запросами
  • Отсутствие взаимодействия с JavaScript

Можно построить ML-модель, которая анализирует не только UA, но и временные паттерны:

<?php
class TemporalBotDetector
{
    private $userPatterns = [];
    
    public function analyzeRequest(string $userId, string $ua, DateTime $timestamp, string $path): array
    {
        if (!isset($this->userPatterns[$userId])) {
            $this->userPatterns[$userId] = [
                'ua_history' => [],
                'request_times' => [],
                'paths' => [],
                'first_seen' => $timestamp
            ];
        }
        
        $pattern = &$this->userPatterns[$userId];
        $pattern['ua_history'][] = $ua;
        $pattern['request_times'][] = $timestamp->getTimestamp();
        $pattern['paths'][] = $path;
        
        // Анализируем паттерн после 10 запросов
        if (count($pattern['request_times']) > 10) {
            return $this->detectBotBehavior($pattern);
        }
        
        return ['is_bot' => false, 'confidence' => 0.0];
    }
    
    private function detectBotBehavior(array &$pattern): array
    {
        $scores = [];
        
        // 1. Постоянство UA (боты редко меняют UA)
        $uniqueUa = count(array_unique($pattern['ua_history']));
        $scores['ua_constancy'] = $uniqueUa === 1 ? 1.0 : 0.0;
        
        // 2. Регулярность интервалов
        $intervals = [];
        for ($i = 1; $i < count($pattern['request_times']); $i++) {
            $intervals[] = $pattern['request_times'][$i] - $pattern['request_times'][$i - 1];
        }
        
        $intervalStd = $this->standardDeviation($intervals);
        $scores['regular_intervals'] = $intervalStd < 0.5 ? 1.0 : 0.0;
        
        // 3. Количество уникальных путей
        $scores['many_paths'] = count(array_unique($pattern['paths'])) > 20 ? 1.0 : 0.0;
        
        // 4. Время суток
        $hours = array_map(function($timestamp) {
            return (int)date('H', $timestamp);
        }, $pattern['request_times']);
        
        $scores['off_hours'] = (int)(min($hours) < 6 || max($hours) > 23);
        
        // Комбинируем скоры
        $botScore = array_sum($scores) / count($scores);
        
        return [
            'is_bot' => $botScore > 0.7,
            'confidence' => $botScore,
            'indicators' => $scores
        ];
    }
    
    private function standardDeviation(array $values): float
    {
        if (count($values) === 0) return 0;
        $mean = array_sum($values) / count($values);
        $variance = array_sum(array_map(function($value) use ($mean) {
            return pow($value - $mean, 2);
        }, $values)) / count($values);
        return sqrt($variance);
    }
}

Проверка согласованности IP-адреса и User-Agent

Можно построить модель ожидаемых UA для разных IP-диапазонов:

<?php
class IPUAConsistencyChecker
{
    private $ipUaDatabase;
    private $geoIpReader;
    
    public function __construct(\GeoIp2\Database\Reader $geoIpReader)
    {
        $this->geoIpReader = $geoIpReader;
        $this->ipUaDatabase = $this->loadGeolocationDb();
    }
    
    public function checkConsistency(string $ip, string $ua): array
    {
        $geoInfo = $this->geoIpReader->city($ip);
        $uaPlatform = $this->extractPlatform($ua);
        
        $inconsistencies = [];
        
        // 1. Мобильные UA с десктопных IP (и наоборот)
        if ($uaPlatform === 'ios' && $geoInfo->traits->userType === 'desktop') {
            $inconsistencies[] = 'mobile_ua_on_desktop_ip';
        }
        
        // 2. ISP соответствие
        if ($this->isDatacenterIp($ip) && stripos($ua, 'bot') === false) {
            $inconsistencies[] = 'datacenter_ip_non_bot_ua';
        }
        
        // 3. Географическая согласованность языка
        if ($geoInfo->country->isoCode === 'CN' && stripos($ua, 'zh-CN') === false) {
            $inconsistencies[] = 'language_mismatch';
        }
        
        return [
            'consistent' => count($inconsistencies) === 0,
            'inconsistencies' => $inconsistencies,
            'risk_score' => count($inconsistencies) * 0.3
        ];
    }
    
    private function extractPlatform(string $ua): string
    {
        if (stripos($ua, 'Windows') !== false) {
            return 'windows';
        } elseif (stripos($ua, 'Mac OS') !== false || stripos($ua, 'Macintosh') !== false) {
            return 'macos';
        } elseif (stripos($ua, 'Linux') !== false && stripos($ua, 'Android') === false) {
            return 'linux';
        } elseif (stripos($ua, 'iPhone') !== false || stripos($ua, 'iPad') !== false) {
            return 'ios';
        } elseif (stripos($ua, 'Android') !== false) {
            return 'android';
        }
        return 'unknown';
    }
    
    private function isDatacenterIp(string $ip): bool
    {
        // Проверяем по базе датацентров (AWS, GCP, Azure, etc.)
        $datacenterRanges = [
            '3.0.0.0/8', // AWS
            '35.0.0.0/8', // GCP
            '13.64.0.0/11', // Azure
        ];
        
        foreach ($datacenterRanges as $range) {
            if (ip_in_range($ip, $range)) {
                return true;
            }
        }
        return false;
    }
    
    private function loadGeolocationDb(): array
    {
        // Загрузка дополнительных баз данных
        return [];
    }
}

Обход распространенных проблем и ложные срабатывания

Проблема 1: Legitimate боты и API-клиенты

Не все запросы без типичного браузерного UA являются вредоносными. К ним относятся:

Поисковые боты:

  • Googlebot
  • bingbot
  • Slurp (Yahoo)
  • DuckDuckBot
  • Baiduspider

Социальные медиа боты:

  • facebookexternalhit
  • TwitterBot
  • LinkedInBot

Legitimate API-клиенты:

  • Мобильные приложения
  • RSS-ридеры
  • Webhook-сервисы

Решение: Многоуровневая верификация

<?php
class BotVerifier
{
    public function verifyLegitimateBot(Request $request): bool
    {
        $ua = $request->getHeaderLine('User-Agent');
        $ip = $request->getServerParams()['REMOTE_ADDR'] ?? '';
        
        // Уровень 1: Проверка UA-строки
        if (stripos($ua, 'Googlebot') === false) {
            return false;
        }
        
        // Уровень 2: Reverse DNS lookup
        $host = gethostbyaddr($ip);
        if ($host === false || (!str_ends_with($host, '.googlebot.com') && !str_ends_with($host, '.google.com'))) {
            return false;
        }
        
        // Уровень 3: Forward DNS подтверждение
        $resolvedIp = gethostbyname($host);
        if ($resolvedIp !== $ip) {
            return false;
        }
        
        // Уровень 4: Проверка диапазона IP Google
        // Google публикует JSON с IP-диапазонами своих ботов
        if (!$this->ipInGoogleRange($ip)) {
            return false;
        }
        
        return true;
    }
    
    private function ipInGoogleRange(string $ip): bool
    {
        $googleIps = $this->fetchGoogleBotIps();
        foreach ($googleIps as $range) {
            if (ip_in_range($ip, $range)) {
                return true;
            }
        }
        return false;
    }
    
    private function fetchGoogleBotIps(): array
    {
        $cacheFile = sys_get_temp_dir() . '/google_bot_ips.json';
        
        // Обновляем раз в сутки
        if (!file_exists($cacheFile) || filemtime($cacheFile) < time() - 86400) {
            $json = file_get_contents('https://www.gstatic.com/ipranges/goog.json');
            file_put_contents($cacheFile, $json);
        }
        
        $data = json_decode(file_get_contents($cacheFile), true);
        return $data['prefixes'] ?? [];
    }
}

Проблема 2: Privacy-focused браузеры

Браузеры, ориентированные на приватность, могут иметь измененные UA:

  • Brave: Маскируется под Chrome, но имеет дополнительные признаки
  • Tor Browser: Использует стандартизированный UA для всех пользователей
  • Firefox в режиме приватности: Может урезать некоторые компоненты

Решение: Альтернативные методы идентификации

<?php
class PrivacyBrowserDetector
{
    public function detectBrave(Request $request): bool
    {
        // Brave добавляет специфичный заголовок
        $ua = $request->getHeaderLine('User-Agent');
        $braveHeader = $request->getHeaderLine('Brave');
        
        return !empty($braveHeader) || strpos($ua, 'Brave') !== false;
    }
    
    public function detectTor(Request $request): bool
    {
        $headers = $request->getHeaders();
        
        // Tor Browser имеет специфичный набор заголовков
        return empty($headers['js-enabled']) &&
               empty($headers['cookies-enabled']) &&
               $request->getHeaderLine('User-Agent') === 'Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0';
    }
}

Проблема 3: Legacy браузеры и системы

Некоторые корпоративные системы используют устаревшие браузеры (IE11, старые версии Chrome/Firefox).

Решение: Грейдированный доступ

<?php
class AccessTierManager
{
    private $corporateIpRanges;
    
    public function __construct()
    {
        $this->corporateIpRanges = $this->loadCorporateRanges();
    }
    
    public function getUserTier(string $ua, string $ipCountry): array
    {
        $browserScore = $this->scoreBrowserModernity($ua);
        $regionRisk = $this->getRegionRiskScore($ipCountry);
        
        // Пользователи из рискованных регионов с устаревшими браузерами получают
        // ограниченный доступ, а не полный запрет
        if ($browserScore < 0.3 && $regionRisk > 0.7) {
            return [
                'tier' => 'highly_restricted',
                'features' => ['read_only', 'no_payments', 'rate_limit_very_strict'],
                'reason' => 'Legacy browser from high-risk region'
            ];
        }
        
        return [
            'tier' => 'full',
            'features' => ['all'],
            'reason' => 'Standard access'
        ];
    }
    
    private function scoreBrowserModernity(string $ua): float
    {
        if (preg_match('/Chrome\/(\d+)/', $ua, $match)) {
            return min($match[1] / 120, 1.0); // Chrome 120 = 1.0
        } elseif (preg_match('/Firefox\/(\d+)/', $ua, $match)) {
            return min($match[1] / 120, 1.0);
        }
        return 0.1; // Устаревший браузер
    }
    
    private function getRegionRiskScore(string $country): float
    {
        $riskMap = [
            'US' => 0.1,
            'DE' => 0.1,
            'CN' => 0.8,
            'RU' => 0.7,
        ];
        return $riskMap[$country] ?? 0.5;
    }
    
    private function loadCorporateRanges(): array
    {
        // Загрузка из конфига
        return [];
    }
}

Проблема 4: Мобильные приложения с кастомными UA

Мобильные приложения часто используют UA формата:

MyApp/1.0 (iOS; iPhone; ru-RU)

Решение: Whitelist для приложений

# В Nginx конфигурации
map $http_user_agent $app_id {
    default "";
    ~*^MyApp/([\d\.]+)\s*\(iOS  "myapp_ios";
    ~*^YourApp/([\d\.]+)\s*\(Android "yourapp_android";
}

server {
    location /api/ {
        # API доступен только для приложений и браузеров
        if ($app_id = "" && $is_modern_browser = 0) {
            return 403;
        }
        
        # Для приложений — специальная обработка
        if ($app_id != "") {
            # Проверяем версию приложения
            set $min_version "1.5.0";
            # ... логика проверки версии ...
        }
        
        proxy_pass http://api_backend;
    }
}
<?php
class MobileAppVerifier
{
    private $allowedApps = [
        'myapp_ios' => ['min_version' => '1.5.0', 'secret_key' => '...'],
        'yourapp_android' => ['min_version' => '2.0.1', 'secret_key' => '...']
    ];
    
    public function verifyAppRequest(Request $request): bool
    {
        $ua = $request->getHeaderLine('User-Agent');
        
        foreach ($this->allowedApps as $appId => $config) {
            if (preg_match("/^$appId\/([\d\.]+)/", $ua, $match)) {
                $version = $match[1];
                
                // Проверяем версию
                if (version_compare($version, $config['min_version'], '<')) {
                    return false;
                }
                
                // Проверяем подпись запроса
                $signature = $request->getHeaderLine('X-App-Signature');
                if (!$this->verifySignature($request, $signature, $config['secret_key'])) {
                    return false;
                }
                
                return true;
            }
        }
        
        return false;
    }
    
    private function verifySignature(Request $request, string $signature, string $secretKey): bool
    {
        // Проверка HMAC подписи
        $data = $request->getUri() . $request->getBody();
        $expectedSignature = hash_hmac('sha256', $data, $secretKey);
        return hash_equals($expectedSignature, $signature);
    }
}

Интеграция с другими системами безопасности

Комбинация с Rate Limiting

User-Agent позволяет создавать интеллектуальные правила rate limiting:

# Отдельные зоны для разных типов клиентов
limit_req_zone $binary_remote_addr$http_user_agent zone=browser:10m rate=100r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=1000r/s;
limit_req_zone $binary_remote_addr zone=bot:10m rate=1r/s;

map $http_user_agent $rate_limit_zone {
    default "browser";
    
    # API-клиенты получают более высокий лимит
    ~*MyApp/\d+\.\d+ "api";
    
    # Поисковые боты — отдельная зона
    ~*Googlebot "bot";
    ~*bingbot "bot";
}

server {
    location / {
        limit_req zone=$rate_limit_zone burst=5 nodelay;
        # ... остальная конфигурация ...
    }
}

Комбинация с WAF

User-Agent анализ может корректировать правила WAF:

<?php
class AdaptiveWAF
{
    private $baseRules;
    
    public function __construct()
    {
        $this->baseRules = $this->loadWafRules();
    }
    
    public function getActiveRules(Request $request): array
    {
        $ua = $request->getHeaderLine('User-Agent');
        $rules = $this->baseRules;
        
        // Для старых браузеров усиливаем правила
        if ($this->isLegacyBrowser($ua)) {
            $rules['sql_injection']['severity'] = 'high';
            $rules['xss']['block_on_match'] = true;
        }
        
        // Для API-клиентов убираем правила, связанные с браузером
        if ($this->isApiClient($ua)) {
            unset($rules['html_injection']);
            unset($rules['javascript_injection']);
        }
        
        // Для новых браузеров — можно ослабить некоторые правила
        if ($this->isVeryModernBrowser($ua)) {
            $rules['csrf']['block_on_match'] = false; // Проверяем на уровне приложения
        }
        
        return $rules;
    }
    
    private function isLegacyBrowser(string $ua): bool
    {
        return preg_match('/MSIE \d|Chrome\/[0-7]\d/', $ua);
    }
    
    private function isApiClient(string $ua): bool
    {
        return preg_match('/MyApp|YourApp|API-Client/', $ua);
    }
    
    private function isVeryModernBrowser(string $ua): bool
    {
        // Chrome 110+, Firefox 110+
        return preg_match('/Chrome\/(11[0-9]|1[2-9]\d)|Firefox\/(11[0-9]|1[2-9]\d)/', $ua);
    }
    
    private function loadWafRules(): array
    {
        // Загружаем правила из конфигурации
        return include __DIR__ . '/waf_rules.php';
    }
}

Комбинация с бот-менеджмент решениями

Cloudflare, Akamai, DataDox и другие предоставляют собственные сигнальные поля, которые включают анализ User-Agent:

<?php
// Cloudflare Workers аналог на PHP (через CF-Connecting-IP и другие заголовки)
class CloudflareIntegration
{
    public function processRequest(Request $request): ResponseInterface
    {
        // Cloudflare добавляет свои заголовки
        $cfRay = $request->getHeaderLine('CF-Ray');
        $cfIpCountry = $request->getHeaderLine('CF-IPCountry');
        $cfThreatScore = (int)$request->getHeaderLine('CF-Threat-Score'); // 0-100
        
        // Cloudflare уже проанализировал UA и дал оценку
        if ($cfThreatScore > 50) {
            // Высокая вероятность бота, несмотря на хороший UA
            return new Response(403, [], 'Bot detected by Cloudflare');
        }
        
        // Дополнительная проверка на нашей стороне
        if ($cfThreatScore > 10 && !$this->isVerifiedBot($request->getHeaderLine('User-Agent'))) {
            return new Response(403, [], 'Suspicious activity');
        }
        
        // Продолжаем обработку
        return $this->nextHandler->handle($request);
    }
    
    private function isVerifiedBot(string $ua): bool
    {
        return preg_match('/Googlebot|bingbot|Slurp/', $ua);
    }
}

Этические и юридические аспекты

Приватность пользователей

User-Agent содержит информацию, которая может считаться личными данными в некоторых юрисдикциях (GDPR, CCPA).

GDPR соображения:

  • User-Agent может использоваться для идентификации устройства
  • Это квалифицируется как "online identifier"
  • Требуется согласие для сбора и обработки

Лучшие практи