Самый известный приём CDP-детекта — геттер на Error.stack, который срабатывает, если клиент автоматизации вызвал Runtime.enable, — перестал работать, когда два коммита в инспектор V8 от мая 2025 года попали в Chrome 138. Невидимыми браузеры под управлением CDP от этого не стали. Исследователи с тех пор нашли другие способы заметить включённый домен Runtime, а коммерческий детект в основном опирается на артефакты фреймворков, признаки headless-режима и окружения, свойства событий ввода, поведенческие и сетевые сигналы — всё то, что никогда не зависело от этой одной проверки.
Что такое CDP-детект и почему уязвимы все крупные фреймворки
CDP-детект — это распознавание того, что браузером управляют через Chrome DevTools Protocol, а не человек. Он работает потому, что протокол создавался для отладки, а некоторые функции отладчика оставляют следы, которые видит JavaScript на странице.
Chrome DevTools Protocol — это WebSocket-API, которым пользуется само окно DevTools: домены Runtime, Page, Network, Input и другие, у каждого свои команды и события. Через него идёт большая часть автоматизации Chromium:
- Puppeteer управляет Chrome через CDP. Для Firefox он теперь использует WebDriver BiDi.
- Playwright использует CDP для Chromium. Для Firefox — Juggler, отдельный протокол внутри Firefox.
- Selenium 4 общается по WebDriver с ChromeDriver, который внутри управляет Chrome через CDP; кроме того, Selenium напрямую открывает доступ к CDP-командам, пока переходит на BiDi.
Firefox от CDP уже отказался. Mozilla объявила свою реализацию CDP устаревшей в Firefox 129 и полностью удалила её в Firefox 141. Поэтому «CDP-детект» в 2026 году означает обнаружение автоматизированных браузеров семейства Chromium.
Чем этот класс сигналов привлекал защитников, DataDome объяснила в исследовании июня 2024 года (перепечатка из её блога об угрозах): «Умение определить, что браузер инструментирован через CDP, — ключ к обнаружению большинства современных бот-фреймворков». Когда новый headless Chrome перешёл на общую кодовую базу с обычным Chrome, большинство несоответствий в отпечатке исчезло. Побочный эффект на уровне протокола ловил бы Puppeteer, Playwright, Selenium и любой неизвестный фреймворк, построенный на той же основе.
Как работала классическая проверка Runtime.enable
Проверка эксплуатировала то, что при включённом домене Runtime инспектор V8 сериализует каждый аргумент console, чтобы отправить клиенту превью. Сериализация объекта читает его свойства, а чтение свойства с геттером запускает код страницы.
Вариант, описанный Castle, выглядел так:
let wasAccessed = false;
const e = new window.Error();
Object.defineProperty(e, 'stack', {
configurable: false,
enumerable: false,
get: function () {
wasAccessed = true;
return '';
}
});
console.debug(e);
// wasAccessed === true => кто-то слушает домен Runtime
По шагам:
- Puppeteer, Playwright и ChromeDriver вызывают
Runtime.enable, потому что им нужны событияRuntime.executionContextCreated, чтобы знать, где выполнять скрипты. Документация протокола описывает команду как включение «отчётов о создании контекстов выполнения». - После включения домена каждый вызов
console.*порождает событиеRuntime.consoleAPICalled, и V8 строит для него превью каждого аргумента. - Для
Errorпостроение превью означало чтение.stack(вместе с.nameи.message). Страница подменила.stackгеттером, поэтому геттер срабатывал и выставлял флаг. - В обычном браузере, к которому ничего не подключено, превью не строится и геттер не вызывается.
Проверка занимала несколько строк JavaScript, почти ничего не стоила и ловила любой фреймворк, вызывающий Runtime.enable, что бы он ни делал с navigator.webdriver или user agent. Stealth-плагины, патчащие свойства JavaScript, исправить это не могли: утечка была в протоколе, а не на странице. Контрмера появилась на уровне драйвера: rebrowser-patches и Patchright не вызывают Runtime.enable, а DataDome в 2024 году отмечала, что nodriver и Selenium Driverless используют «низкоуровневые CDP-команды, не задействующие Runtime.enable».
Что изменилось в V8 в 2025 году
V8 закрыл этот побочный эффект двумя коммитами в мае 2025 года: инспектор перестал запускать пользовательские геттеры при построении превью объектов и ошибок. Исправление вошло в V8 13.8, который вышел в Chrome 138 (стабильная версия — 24 июня 2025 года).
61a90754(7 мая 2025), «[inspector] Prevent side effects during object preview» — исключает «запуск пользовательского кода при включении домена runtime».e08e9734(9 мая 2025), «[inspector] Prevent side effects during error preview» — меняет генерацию описания ошибок, «чтобы не запускать геттеры с побочными эффектами».
Оба коммита ссылаются на баг Chromium 415094795. Автор первого — Павел Фельдман, мейнтейнер Playwright в Microsoft, то есть исправление пришло со стороны инструментов автоматизации, а не от stealth-проекта. Антуан Вастель из Castle разобрал это 28 августа 2025 года в статье «Why a classic CDP bot detection signal suddenly stopped working (and nobody noticed)», указав на новый хелпер getErrorProperty(), который пропускает пользовательские геттеры при построении превью. В Chrome 138 и новее сниппет выше возвращает false даже под стоковым Puppeteer.
Можно ли всё ещё обнаружить Runtime.enable?
Скорее всего да, хотя и менее надёжно, чем раньше. В марте 2026 года независимый исследователь Sveba опубликовал анализ пути построения превью в V8, где показал два оставшихся маршрута. Первый — ветка кода, которую майская защита 2025 года пропускает, когда GetOwnPropertyDescriptor завершается неудачей; по словам автора, он «проверил… что это эксплуатируемо в Chrome после патча». Второй использует Proxy, помещённый в цепочку прототипов объекта:
let detected = false;
const trap = new Proxy({}, {
ownKeys() { detected = true; return []; }
});
const obj = Object.create(trap);
console.groupEnd(obj); // подойдёт любой метод console
// detected === true => инспектор строит превью
Согласно разбору, инспектор проверяет только, является ли Proxy само значение, а не его прототипы, и собирает все ключи сразу. Поэтому обход цепочки вызывает ловушку ownKeys. Автор тестировал это на сборке content_shell от марта 2026 года и сообщил, что на тот момент уязвимость не была закрыта. Мы не проверяли это на каждом стабильном релизе Chrome, так что считайте это заявленным, а не универсальным результатом.
Две оговорки ограничивают то, насколько защитник может сейчас полагаться на любой сигнал Runtime.enable. Во-первых, реальный пользователь с открытыми DevTools проходит через тот же путь кода, поэтому сигнал говорит «подключён инспектор», а не «это бот». Во-вторых, фреймворки, которые важнее всего поймать, уже избегают Runtime.enable, так что сигнал в основном отмечает немодифицированные Puppeteer, Playwright и Selenium. Индустрия перешла к сочетанию множества более слабых сигналов — об этом пишет и Castle.
Сигналы, которые работают в 2026 году
Сигналы, которые сегодня держатся, связаны с тем, что фреймворк делает на странице, как браузер собран и запущен, как поступает ввод и как сессия ведёт себя в сети. Сокрытие одного побочного эффекта CDP ни один из них не убирает.
Артефакты фреймворка в main world
Стоковые фреймворки оставляют собственные отпечатки. rebrowser bot detector перечисляет несколько:
__pwInitScripts: Playwright по умолчанию добавляет его в глобальную область каждой страницы.- Утечки
sourceURL: Puppeteer помечает выполняемые скрипты как//# sourceURL=pptr:…, и эта метка может появиться в стеке ошибки, снятом внутри перехваченной функции. - Выполнение в main world: если ваш скрипт вызывает
document.querySelectorв собственном мире страницы, страница, обернувшая эту функцию, видит вызов. exposeFunction/ bindings:page.exposeFunction()иwindow.__playwright__binding__в Playwright создают глобальные объекты, которые страница может перечислить. Под капотом используетсяRuntime.addBinding, который добавляет функцию «в глобальные объекты всех инспектируемых контекстов».- Имена utility world: фреймворки создают изолированные миры с узнаваемыми именами; rebrowser-patches позволяет их переименовать.
Артефакты init-скриптов
Page.addScriptToEvaluateOnNewDocument выполняет скрипт «в каждом фрейме при его создании (до загрузки скриптов фрейма)». Так stealth-плагины патчат navigator до того, как запустится код сайта. Протокол здесь ничего не выдаёт, а вот патчи — выдают. Как сказано в README Camoufox, внедрённый JavaScript можно поймать через Object.getOwnPropertyDescriptor, по тому, что toString() функции больше не возвращает [native code], или по значениям, которые различаются в окне и в воркере.
Контексты воркеров
Воркеры — место, где патчи ломаются чаще всего. Init-скрипты и переопределения свойств, применённые к странице, не попадают автоматически в dedicated, shared и service workers. Воркер, сообщающий настоящие navigator.userAgent, hardwareConcurrency или рендерер WebGL, когда окно показывает подменённые значения, — сильный признак. CreepJS проверяет область воркеров именно поэтому.
Признаки headless-режима и сборки
- Headless shell против new headless. Chrome запустил
--headless=newв Chrome 112, а начиная с Chrome 132 старый режим живёт только в отдельном бинарникеchrome-headless-shell. Playwright в headless-режиме по умолчанию использует chromium headless shell, а тот ведёт себя иначе, чем настоящий Chrome. - Идентичность сборки. User agent «Chrome for Testing», вьюпорты по умолчанию (800×600 в Puppeteer, 1280×720 в Playwright) и невыключенный
navigator.webdriver— всё это дешёвые проверки из списка rebrowser detector. - Кодеки. Открытые сборки Chromium не содержат проприетарных кодеков вроде H.264 и AAC, которые есть в Google Chrome, по данным проекта Chromium. Браузер, который называет себя Chrome, но не проходит
canPlayTypeдля H.264, на самом деле Chromium. - Разрешения и медиаустройства. В headless-режиме и в контейнерах состояние разрешения на уведомления, пустой список медиаустройств и отсутствие голосов синтеза речи часто не соответствуют заявленному user agent. CreepJS и BrowserScan это проверяют.
Синтетический ввод
Ввод, отправленный через Input.dispatchMouseEvent, порождает доверенные события (isTrusted равно true), но у них были свойства, которых нет у настоящего ввода. Самое известное — screenX/screenY, равные pageX/pageY вне полноэкранного режима. Chromium исправил это в изменении 6917162 «Fix screen coordinates to avoid automation detection», влитом 15 сентября 2025 года, до точки ветвления Chrome 142. Проект CDP-Patches, созданный ради обхода этой проблемы, теперь сообщает, что CDP-ввод генерирует и coalesced-события, и сам себя архивировал. В актуальном Chrome эти две конкретные проверки в основном ушли, а анализ траектории, тайминга и точек клика по-прежнему работает.
Поведение и сеть
Ни один из сигналов ниже не связан с CDP, и именно они составляют основную часть того, что оценивают вендоры. Castle перечисляет клиентский поведенческий анализ, «красные таблетки» окружения, обнаружение прокси и сети, а также анализ временных рядов скоординированной активности. Со стороны сети это TLS-отпечатки и HTTP/2-отпечатки, которые должны соответствовать заявленному браузеру, плюс репутация IP. Фингерпринтинг на уровне GPU мы разбираем в подробном материале о canvas и WebGL.
Как Patchright, nodriver и Camoufox справляются с утечками CDP
Все три избегают утечек в духе Runtime.enable, каждый по-своему, и ни один не решает проблему поведения и сетевой репутации.
Patchright — пропатченный драйвер Playwright только для Chromium. Согласно README, его главный патч обходит Runtime.enable «за счёт выполнения JavaScript в (изолированных) ExecutionContexts». Кроме того, он полностью отключает домен Console (поэтому логирование в консоль не работает), убирает --enable-automation, добавляет --disable-blink-features=AutomationControlled и рекомендует запускать настоящий Chrome (channel="chrome") без внедрения отпечатков. Он внимательно следует за релизами Playwright (драйвер v1.63.0 вышел в сентябре 2026 года). Настройку описывает наш подробный разбор Patchright.
nodriver, преемник undetected-chromedriver, отказывается от WebDriver и общается по CDP с установленным у вас Chrome. Просмотр текущих исходников в core/ показывает, что он выполняет код через Runtime.evaluate и Runtime.callFunctionOn и ни разу не вызывает Runtime.enable — это совпадает с описанием DataDome 2024 года. Компромисс в том, что выполнение идёт в main world, где страница, обернувшая DOM-функции, может его увидеть, а управления отпечатками нет. Последний релиз на PyPI — 0.50.3 от мая 2026 года. Подробнее — в нашем руководстве по nodriver.
Camoufox обходит CDP целиком. Это форк Firefox, а Playwright управляет Firefox через Juggler, который Camoufox патчит так, что Playwright работает с изолированной копией страницы: скрипты страницы не видят внедрённых хелперов и не могут отследить чтение геттеров. Ввод идёт через штатные обработчики Firefox, а значения отпечатка подменяются в C++, а не init-скриптами, что снимает весь класс проблем с toString и расхождениями в воркерах. Цена — это Firefox. README предупреждает, что некоторые WAF проверяют поведение движка SpiderMonkey, так что за Chrome он себя не выдаст.
Сводная таблица сигналов
| Сигнал | Работает в 2026 году? | Что его обходит | Цена обхода |
|---|---|---|---|
Геттер Error.stack через console (классика) | Нет в Chrome 138+ (V8 13.8) | Актуальный Chrome | Нулевая |
| Другие побочные эффекты превью инспектора (например, Proxy в цепочке прототипов) | По данным на март 2026 года работает; срабатывает и у реальных пользователей с открытыми DevTools | Отказ от вызова Runtime.enable (Patchright, nodriver, rebrowser-patches) или от CDP вообще (Camoufox) | Низкая или средняя: патч драйвера, потеря console API |
__pwInitScripts, bindings, sourceURL pptr: | Да, против стоковых фреймворков | Пропатченные драйверы; отказ от exposeFunction | Низкая |
| Перехваты выполнения в main world | Да | Выполнение в изолированном мире | Средняя: нет прямого доступа к глобальным объектам страницы |
Свойства, пропатченные в JS (toString, дескрипторы) | Да | Не патчить; использовать настоящий Chrome или подмену на уровне C++ | От средней до высокой |
| Расхождение окна и воркера | Да | То же; патчи должны покрывать воркеры | Высокая, если патчить в JS |
| Headless shell / Chrome for Testing / кодеки | Да | Брендовый Chrome, headful или new headless, виртуальный дисплей | Средняя: больше RAM, Xvfb на серверах |
| CDP-ввод: screenX == pageX, нет coalesced-событий | В актуальном Chrome в основном исправлено | Актуальный Chrome | Нулевая |
| Поведение и тайминг | Да, и доля в скоринге растёт | Реалистичный темп, меньше запросов на одну идентичность | Высокая: пропускная способность |
| TLS / HTTP/2 / репутация IP | Да | Сетевой стек настоящего браузера, чистые резидентные или ISP-адреса | Постоянные расходы на прокси |
Как протестировать свою конфигурацию
Сначала тестируйте на собственной пробной странице, затем на публичных страницах детекта и меняйте по одной переменной за раз. Публичные страницы показывают, что обнаруживают они. Ваша страница показывает, какое сочетание сборки Chrome и драйвера ещё срабатывает на какую проверку.
1. Локальная пробная страница
Сохраните это как probe.html. Страница выполняет классическую проверку, проверку через цепочку прототипов и несколько дешёвых проверок артефактов, а затем пишет результаты в DOM, чтобы их мог прочитать любой драйвер, в каком бы мире ни выполнялись его скрипты.
<!doctype html>
<pre id="out">running</pre>
<script>
function classic() {
let hit = false;
const e = new Error();
Object.defineProperty(e, 'stack', { configurable: false, enumerable: false,
get() { hit = true; return ''; } });
console.debug(e);
return hit;
}
function protoProxy() {
let hit = false;
const trap = new Proxy({}, { ownKeys() { hit = true; return []; } });
console.groupEnd(Object.create(trap));
return hit;
}
const r = {
classicStackGetter: classic(),
protoProxyPreview: protoProxy(),
webdriver: navigator.webdriver,
pwInitScripts: '__pwInitScripts' in window,
h264: document.createElement('video').canPlayType('video/mp4; codecs="avc1.42E01E"'),
ua: navigator.userAgent,
};
setTimeout(() => { document.getElementById('out').textContent = JSON.stringify(r, null, 2); }, 300);
</script>
Затем запустите её под каждой интересующей вас конфигурацией:
import pathlib
from playwright.sync_api import sync_playwright # замените на: from patchright.sync_api import sync_playwright
url = pathlib.Path("probe.html").resolve().as_uri()
with sync_playwright() as p:
browser = p.chromium.launch(channel="chrome", headless=False)
page = browser.new_page()
page.goto(url)
page.wait_for_function("document.getElementById('out').textContent !== 'running'")
print(page.text_content("#out"))
browser.close()
Прогоните стоковый Playwright, затем Patchright, затем ту же страницу в обычном окне Chrome с открытыми DevTools и без них. Прогон с DevTools — ваш контроль: всё, что срабатывает там, сработает и у реальных пользователей, так что само по себе это слабый сигнал.
2. Публичные страницы детекта
- rebrowser bot detector: ориентирован на Chromium, покрывает Runtime.enable, sourceURL, выполнение в main world,
exposeFunction,__pwInitScripts, вьюпорт и user agent. Некоторые тесты срабатывают, только если ваш скрипт вызывает на страницеdummyFnили пробрасывает функцию, — это объясняется на самой странице. - BrowserScan bot detection: проверки WebDriver, user agent, CDP и согласованности navigator.
- CreepJS: открытый анализатор отпечатков, который отмечает «ложь» (пропатченные свойства), признаки headless-режима и расхождения в области воркеров. Лучше всех ловит подмену на уровне JavaScript.
- Device and Browser Info «Are you a bot?»: тесты отпечатка и поведения, включая проверки CDP, WebDriver, Playwright и headless Chrome.
Проходите их через тот же прокси, что и в продакшене. Некоторые показывают IP и часовой пояс рядом с браузерными сигналами, а идеальная оценка браузера на IP из дата-центра мало что говорит о реальном трафике. Sticky-сессия (USERNAME-sid-abc123-ttl-10m на резидентном шлюзе) фиксирует IP, пока вы сравниваете прогоны.
Помните: пройти тестовую страницу — не то же самое, что пройти сайт. Коммерческие вендоры используют серверные модели, историю между сессиями и сигналы, которых эти страницы не видят.
Чего это не решает
Закрытие утечек CDP убирает одну категорию улик. Автоматизация от этого не начинает выглядеть по-человечески, а сбор данных не становится разрешённым. Темп, последовательность и объём запросов на одну идентичность по-прежнему выдают автоматизацию, а репутация IP и TLS всё так же должны соответствовать браузеру, которым вы представляетесь. Правила доступа никуда не деваются: robots.txt, условия использования и законы о защите данных для всего, что касается персональной информации. Если вы занимаетесь детектом, практический урок 2025 года — не зависеть от одного побочного эффекта, который вендор браузера может убрать рутинным коммитом. Если вы занимаетесь автоматизацией, держите инструменты в актуальном состоянии, выбирайте драйверы, которые не патчат JavaScript, и исходите из того, что оставшиеся сигналы — поведенческие.






