Узнайте, как сайты обнаруживают Selenium, Puppeteer и Playwright через navigator.webdriver, артефакты CDP, отсутствующие функции и рендеринг SwiftShader.
Headless-браузеры лежат в основе огромной доли веб-автоматизации: наборы CI-тестов, мониторинг доступности, парсеры цен — и, с другой стороны, брутфорс учётных данных, скальперы и боты для накрутки кликов. Обнаружение давно переросло эпоху, когда строка User-Agent буквально содержала HeadlessChrome. Сегодня сайты ловят headless-автоматизацию через многоуровневую систему сигналов, которые браузер под управлением кода неизменно оставляет. Это руководство объясняет, какие именно это сигналы и почему их так сложно заглушить.
Ключевые выводы
navigator.webdriver— канонический флаг автоматизации: любой браузер под управлением WebDriver выставляет его вtrue; неправильное патчирование само по себе становится отпечатком.- Артефакты CDP крайне сложно полностью стереть: Chrome DevTools Protocol оставляет характерные глобальные переменные и временны́е паттерны, которые выживают после многих stealth-патчей.
- Функциональные пробелы раскрывают headless-среды: программные WebGL-рендереры, отсутствующие аудиокодеки, противоречивые состояния разрешений и пустой список плагинов — всё это выглядит иначе, чем в реальном настольном браузере.
- Selenium, Puppeteer и Playwright оставляют различимые сигнатуры даже после подмены User-Agent — у каждого фреймворка есть известные артефакты в глобальной области видимости и разное поведение сети по умолчанию.
- Stealth-плагины повышают планку, но не закрывают её: они патчат утечки на уровне JavaScript, однако не могут затронуть сигналы ниже этого уровня — TLS-отпечатки, порядок HTTP/2-фреймов или дерево процессов ОС.
Что такое headless-браузер?
Headless-браузер запускает полноценный движок рендеринга — парсинг HTML, CSS-раскладку, выполнение JavaScript — не отрисовывая ничего на видимом экране. Программа отправляет инструкции через протокол управления, браузер отвечает, и присутствие человека не требуется. Puppeteer, Playwright и Selenium WebDriver работают именно так — как правило, поверх Chrome Headless (запущенного с флагом --headless) или Firefox в headless-режиме.
То же свойство, которое делает headless-браузеры удобными для тестирования, делает их обнаруживаемыми: ими управляет код, а код оставляет следы.
Признак navigator.webdriver
Самый прямой сигнал headless — navigator.webdriver. Спецификация WebDriver требует, чтобы любой браузер под управлением WebDriver выставлял это свойство в true. В реальном настольном браузере оно возвращает false или равно undefined.
// A detector reads this in a single line.
if (navigator.webdriver === true) {
// Browser is almost certainly under WebDriver control.
}
Сам по себе это слабый сигнал — stealth-патчи глушат его уже много лет. Но неправильное патчирование само становится уликой. Если дескриптор свойства выглядит нестандартно, функция get сериализуема или Object.getOwnPropertyDescriptor(navigator, 'webdriver') возвращает объект, отличающийся от того, что отдаёт реальный браузер, — внимательный детектор это заметит. Небрежно запатченный navigator.webdriver = false зачастую выглядит подозрительнее, чем незапатченный true.
Артефакты CDP и DevTools Protocol
Puppeteer и Playwright управляют Chrome через Chrome DevTools Protocol — тот же протокол, что лежит в основе Chrome DevTools. Этот управляющий канал оставляет артефакты на странице.
Утечки в глобальный скоуп
Некоторые паттерны использования CDP внедряют объекты в глобальную область видимости страницы. Исторические версии Puppeteer выставляли __puppeteer_evaluation_script__; другие фреймворки оставляют __nightmare, _phantom или window.callPhantom. Скрипты обнаружения перебирают эти имена:
const knownArtifacts = [
'__nightmare', '_phantom', 'callPhantom',
'__puppeteer_evaluation_script__',
'__selenium_evaluate', '__webdriver_evaluate',
];
const found = knownArtifacts.filter(k => k in window);
// Any hit is a strong automation signal.
Аномалии обработчиков событий
Фреймворки автоматизации, перехватывающие fetch или XMLHttpRequest для модификации ответов, оставляют следы в карте обработчиков событий. Детектор, запрашивающий getEventListeners на объекте window, может увидеть стеки слушателей, которых не бывает в реальной пользовательской сессии.
Временны́е побочные каналы
Коммуникация через CDP имеет измеримую задержку. Последовательность задач, хронометрируемая через performance.now(), может обнаружить характерный джиттер, возникающий, когда каждый шаг требует круговой поездки через CDP-соединение, а не выполняется синхронно в цикле событий браузера.
Сигнатуры Selenium, Puppeteer и Playwright
Все три фреймворка в итоге управляют браузером, но каждый оставляет разные следы.
| Фреймворк | Интерфейс управления | Характерные артефакты |
|---|---|---|
| Selenium | W3C WebDriver по HTTP | navigator.webdriver = true (незапатченный), специфичный для WebDriver порядок HTTP-заголовков от клиентской библиотеки |
| Puppeteer | Chrome DevTools Protocol | Глобальные инъекции CDP, устаревшие переменные window.__puppeteer_*, работает только с Chrome, поэтому TLS совпадает с реальным Chrome |
| Playwright | CDP (Chrome/Edge), внутренние протоколы (Firefox/WebKit) | Более чистые настройки по умолчанию, чем у Puppeteer; варианты Firefox/WebKit расходятся с реальными браузерами по набору функций |
Playwright вложил больше усилий в сокращение низкоуровневых утечек, поэтому исследования в области обнаружения теперь сосредоточены на более тонких сигналах, описанных выше, когда подозреваемым фреймворком является Playwright.
Отсутствующие и противоречивые функции браузера
Headless-среды стабильно отличаются от реальных настольных браузеров сразу в нескольких областях. Скрипты обнаружения зондируют эти пробелы в совокупности.
Список плагинов
В реальном настольном Chrome есть набор плагинов (PDF Viewer и др.), доступных через navigator.plugins. Исторически headless Chrome возвращал пустой массив. В современных сборках это улучшили, но сами объекты плагинов по-прежнему отличаются дескрипторами свойств и могут быть сопоставлены с тем, что открывает браузер реального пользователя.
Медиакодеки
Реальные браузеры договариваются о кодеках с ОС и GPU. Headless-среды без дисплейного оборудования нередко сообщают об ограниченной поддержке кодеков через HTMLMediaElement.canPlayType(). Браузер, утверждающий, что он Chrome на Windows, но сообщающий о неспособности воспроизвести H.264-видео, — значимое противоречие.
WebGL и GPU-рендеринг
WebGL — один из самых сильных маркеров headless. Без доступа к физическому GPU Chrome Headless откатывается к SwiftShader — программному растеризатору. Детектор читает gl.getParameter(gl.RENDERER) и проверяет наличие строк вроде SwiftShader или Google SwiftShader вместо реального названия GPU:
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
const renderer = gl.getParameter(gl.RENDERER);
// "ANGLE (Intel, ...)" or "Apple M2" → real hardware
// "Google SwiftShader" → headless software render
User-Agent, заявляющий мощное устройство, при этом WebGL-рендерер сообщает о чисто программном рендеринге — прямое противоречие.
Противоречия в Permissions API
Permissions API позволяет скриптам запрашивать, предоставил ли пользователь доступ к определённым возможностям. В headless-средах внутреннее хранилище разрешений может находиться в состояниях, невозможных в реальной браузерной сессии, — например, уведомления одновременно denied и prompt. Скрипты обнаружения, зондирующие несколько разрешений и проверяющие логическую согласованность, улавливают такие невалидные комбинации.
Как stealth-плагины пытаются (и часто не могут) скрыться
Библиотеки вроде puppeteer-extra-plugin-stealth патчат многие из перечисленных сигналов прежде, чем их успевает прочитать любой скрипт обнаружения: они переопределяют navigator.webdriver, внедряют реалистичные списки плагинов, заглушают CDP-специфичные глобальные переменные и нормализуют ответы Permissions API. Против наивного обнаружения это работает хорошо.
Проблема носит структурный характер. Во-первых, каждый патч — потенциальная улика: обёртки Object.defineProperty дают другой вывод toString(), чем нативные свойства, а зондирование Object.getOwnPropertyDescriptor(navigator, 'webdriver') обнаруживает искусственный дескриптор. Системы обнаружения теперь расценивают само наличие патча как сигнал. Во-вторых, stealth-плагины работают исключительно на уровне JavaScript. Они не могут затронуть:
- TLS ClientHello отпечаток — наборы шифров и расширения, которые браузер предлагает в рукопожатии, отражают нижележащую сетевую библиотеку (BoringSSL в Chromium), а не уровень JavaScript. Headless Chrome и реальный Chrome имеют одинаковый TLS-отпечаток, а вот Python-скрипт, притворяющийся Chrome, — нет.
- Порядок HTTP/2-фреймов — последовательность и приоритет HTTP/2-фреймов различаются между реальными браузерами и клиентами автоматизации, построенными поверх них.
- Дерево процессов ОС — серверная проверка, видящая процесс ChromeDriver или geckodriver рядом с процессом браузера, располагает внеполосными свидетельствами автоматизации, которые никакой JavaScript-патч не устранит.
Тестирование собственной автоматизации
Если вы используете headless-автоматизацию в легитимных целях — CI-пайплайны, аудиты доступности, проверка доступности — вы можете увидеть именно то, что видят системы обнаружения о ваших сессиях. Инструмент BrowserInsight определения ботов в реальном времени показывает состояние navigator.webdriver, количество плагинов, WebGL-рендерер и другие сигналы автоматизации. Проверка ядра раскрывает, какой движок рендеринга реально работает под строкой User-Agent. Используйте эти инструменты для аудита настройки перед выходом в продакшн.
О сетевых сигналах, которые JavaScript не может увидеть, рассказывает дополняющая статья о техниках обнаружения ботов, посвящённая TLS и HTTP/2-фингерпринтингу.
Часто задаваемые вопросы
Обманывает ли патчирование navigator.webdriver системы обнаружения?
Только самые простые проверки. Отсутствие этого свойства ожидаемо в реальном браузере, но важен способ его отсутствия. Нативный false и запатченный false выглядят по-разному, когда детектор читает дескриптор свойства. Добавьте к патчингу проверки WebGL и плагинов — и оставшегося сигнала достаточно для классификации большинства автоматизаций.
Может ли Playwright лучше уклоняться от обнаружения, чем Selenium?
Дефолтные настройки Playwright чище, чем у классического Selenium, а кросс-браузерная поддержка означает, что драйверы Firefox и WebKit избегают некоторых CDP-артефактов, специфичных для Chrome. Но системы обнаружения отслеживают Playwright-специфичные сигналы точно так же, как сигналы Selenium. Ни один фреймворк сегодня не невидим для стека обнаружения, тестирующего полный набор сигналов.
Почему headless Chrome сообщает о SwiftShader?
SwiftShader — программный CPU-рендерер Chrome, используемый при отсутствии реального GPU. Сервер или контейнер, запускающий headless Chrome, как правило, не имеет физического дисплейного адаптера, поэтому GPU откатывается к SwiftShader. Реальный настольный Chrome сообщает фактическое название GPU-драйвера: Intel, NVIDIA, AMD или Apple Silicon.
Существуют ли headless-настройки, проходящие все проверки?
Теоретически да, на практике — крайне сложно. Потребуется: устранить утечки на уровне JavaScript (stealth-плагины справляются с большинством), обеспечить реальный доступ к GPU (исправляет WebGL), использовать реальные сетевые стеки Chrome с корректным TLS (в целом нормально для автоматизации на базе Chrome) и генерировать реалистичный поведенческий шум в масштабе. Коммерческие антидетект-браузеры пытаются это реализовать; в нашей статье об обнаружении антидетект-браузеров рассказывается, как сайты реагируют на эти инструменты.


