Как сайты решают, что вы бот
Headless-браузеры и скрипты автоматизации оставляют следы в navigator, объекте window, протоколе DevTools и на сетевом уровне. Разбираем, откуда берется каждый сигнал, почему ни один из них не надежен сам по себе и почему под подозрение попадают и обычные люди.
6 мин чтения · Опубликовано 10 сентября 2026 г.
Вы открываете сайт, еще ни на что не нажали, а крутящийся блок Cloudflare уже решил вас пропустить. На каком основании? На основании кучи мелких сигналов, каждый из которых по отдельности ненадежен, но вместе, с весами, они дают достаточно уверенный вердикт. Наша карточка обнаружения ботов использует самую устойчивую горстку сигналов из той же кучи. Ниже — откуда берется каждый из них.
Три вердикта
Системы оценки риска обычно раскладывают посетителей по трем корзинам. Поисковые краулеры (Googlebot, Bingbot, Baiduspider) и мониторы доступности — «хорошие боты»: они открыто представляются, обратный DNS подтверждает, кто они, и большинство сайтов им рады. Скрипты под управлением Selenium, Puppeteer или Playwright, а также краулеры, выдающие себя за поисковики, — «плохие боты»: именно они занимаются подбором паролей, массовой регистрацией, злоупотреблением купонами и скрейпингом. Все остальные — «вероятно, люди».
Обратите внимание на слово «вероятно». Ни один сигнал не доказывает, что за клавиатурой сидит человек. Максимум, что может сказать система, — что это окружение не похоже на управляемое скриптом.
navigator.webdriver
Самый грубый сигнал. Спецификация W3C WebDriver требует, чтобы navigator.webdriver было равно true всякий раз, когда браузер работает под автоматизацией. Браузеры, запущенные из Selenium, Playwright и Puppeteer, по умолчанию так и делают, поэтому проверка занимает одну строку кода.
Естественно, первое, что делает любой автор скрейпера, — убирает этот флаг. Stealth-плагины переопределяют свойство через Object.defineProperty или отключают его флагами запуска. Но небрежное удаление оставляет новые следы. Object.getOwnPropertyDescriptor(navigator, 'webdriver') должен возвращать undefined, потому что свойство живет в прототипе; после переопределения он возвращает дескриптор. Переписанный геттер больше не приводится к строке function get webdriver() { [native code] }. Проверка смещается с вопроса «выставлен ли флаг» к вопросу «трогал ли флаг кто-нибудь».
Что пропало из Chrome
У обычного настольного Chrome почти всегда есть несколько вещей: объект window.chrome (с runtime, loadTimes и прочим), три-пять встроенных записей в navigator.plugins (PDF Viewer, Chromium PDF Plugin и так далее) и непустой navigator.languages.
У раннего headless Chrome не было window.chrome, список плагинов был пуст, а иногда пустовал и список языков. Его UA прямо содержал HeadlessChrome; PhantomJS до него тоже называл себя по имени. Это были первые приемы обнаружения — и первые, которые залатали. Новый headless-режим (--headless=new) сейчас почти неотличим от Chrome с окном.
С числом плагинов, впрочем, нужна осторожность. У мобильного Chrome плагинов нет по замыслу, а Firefox давно перестал показывать список плагинов. Так что «ноль плагинов» — сигнал только при условии «заявляет, что это настольный Chrome»; в остальных случаях это чистое ложное срабатывание. Поэтому в нашей оценке подлинности он стоит всего пять баллов.
Окна и разрешения, противоречащие сами себе
В headless-окружении нет настоящего окна, поэтому window.outerWidth и outerHeight часто равны 0 или их соотношение с innerWidth/innerHeight лишено смысла: область содержимого больше рамки окна или screen.availWidth превышает screen.width. Браузер, на который смотрит человек, так себя не ведет.
Еще одно классическое противоречие связано с разрешением на уведомления: Notification.permission возвращает denied, а navigator.permissions.query({name: 'notifications'}) говорит prompt. В нормальном браузере оба API согласны друг с другом; расходятся они только в некоторых headless-конфигурациях.
Следы CDP
Puppeteer и Playwright управляют браузером через Chrome DevTools Protocol, и подключенный протокол имеет побочные эффекты. Самый известный: когда DevTools сериализует объект Error, он читает его свойство stack. Скрипт обнаружения может создать Error, повесить геттер на stack и передать объект в console.debug(). Без DevTools геттер никогда не сработает; с подключенным CDP — сработает. Аналогично Runtime.enable оставляет заметные изменения в контекстах выполнения страницы.
При всей своей изящности эти приемы — главный источник ложных срабатываний: нажмите F12 и откройте DevTools сами — и вы вызовете их точно так же. Поэтому мы используем только наименее подверженные ошибкам из них и помечаем результат как ориентировочный. Другая сторона тоже не стоит на месте: новые версии Puppeteer и различные патчи обходятся без Runtime.enable, и соревнование продолжается.
Поведение и сеть: сигналы за пределами скрипта
Все перечисленное выше — статические свойства JS-окружения. Основная работа в настоящих системах оценки риска происходит в двух других местах.
Поведение. Траектории мыши у человека изгибаются и дрожат, задерживаются над элементами, замирают в нерешительности. Скриптовые траектории идут из A в B по прямой, а интервалы между кликами ровные, как метроном. Ритм набора текста, ускорение прокрутки и время на странице сравниваются с тем, как на самом деле ведут себя люди.
Сеть. «Обычный пользователь», пришедший с IP дата-центра (AWS, Google Cloud, любой VPS-провайдер), странен сам по себе; см. статью о качестве IP. Еще жестче — TLS-отпечатки. Библиотека requests для Python согласовывает TLS с таким порядком наборов шифров и списком расширений (ее отпечаток JA3/JA4), которые ничем не напоминают Chrome. Добавьте к этому UA от Chrome — и маскировка проваливается на уровне TLS, еще до того, как выполнится хоть строка JavaScript.
Собираем вместе
У каждого сигнала выше есть контрпримеры. webdriver можно убрать, число плагинов подделать, траекторию мыши зашумить, IP дата-центра заменить на резидентный прокси. Поэтому Turnstile, reCAPTCHA v3 и коммерческие SDK оценки риска не проверяют правила по одному. Они взвешивают каждый сигнал, смотрят, подтверждают ли сигналы друг друга, скармливают все это моделям, обученным на больших объемах трафика, и выдают оценку. Высокая оценка проходит молча, средняя получает проверку (спиннер, сетку картинок), низкая блокируется.
По этой же причине «обход обнаружения» — вечная гонка вооружений: исправьте один сигнал, и вес остальных вырастет.
Ложные срабатывания: люди, принятые за ботов
Если посмотреть с другой стороны — какие настоящие люди попадают под подозрение по ошибке?
- Пользователи расширений для приватности. Такие расширения меняют свойства
navigator, блокируют canvas, опустошают список плагинов — и сами эти изменения выглядят как маскировка. - Корпоративные машины. Групповые политики отключают плагины, фиксируют язык, стандартизируют виртуальное окружение, и целый парк машин делит один отпечаток.
- Старые или нишевые браузеры и настольный Linux. Выборок мало, поэтому модели менее уверены.
- Разработчики, отлаживающие с открытыми DevTools. Срабатывает каждая проверка в стиле CDP.
Поэтому наша карточка не сводит ответ к «да/нет». Она перечисляет, какие сигналы сработали. Глядя на список, вы понимаете, какое расширение или настройка за это отвечает, вместо того чтобы смотреть на красный крестик.
Вопросы и ответы
Я не пишу скрейперы. Почему меня это должно волновать? Потому что ложные срабатывания достаются вам. Расширение для приватности, выход через VPN и настольный Linux вместе — этого достаточно, чтобы заметная часть сайтов постоянно показывала вам CAPTCHA. Знание того, какой сигнал сработал, позволяет подкрутить что-то конкретное.
Достаточно ли залатать navigator.webdriver? Нет, а небрежный патч еще и добавляет сверху сигнал «вмешательство». Современное обнаружение оценивает общую согласованность, а не какой-то один флаг.
Отправляет ли проверка на ботов на этом сайте мои данные куда-либо? Нет. Все проверки выполняются локально в вашем браузере. Только если вы согласились участвовать в статистике, мы загружаем хеши атрибутов — никогда не сырые значения.