Маскировка CDP: Почему Playwright и Puppeteer детектируются за секунды
Готовы защитить свою цифровую личность?
Выберите тариф и запускайте незаметные профили уже сегодня.
Chrome DevTools Protocol (CDP) — стандартный интерфейс для автоматизации браузеров. Playwright, Puppeteer и большинство Selenium-стеков используют его для управления Chromium-инстансами. Проблема проста: каждое CDP-подключение оставляет следы, которые антифрод-системы обнаруживают в течение миллисекунд после загрузки страницы. Никакое количество стелс-плагинов, аргументов запуска или runtime-патчей не устраняет эти следы в Chromium полностью — и индустрия детекции точно знает, где искать.
В этой статье мы каталогизируем полный набор векторов детекции CDP по состоянию на 2026 год, объясним, почему поверхностные патчи не работают, и опишем, что на самом деле представляет собой модификация браузера на уровне ядра.
Поверхность детекции: полная инвентаризация
Детекция CDP — это не единичная проверка. Это многоуровневая система из десятков сигналов, каждый из которых по отдельности слаб, но вместе — разоблачителен. Мы можем разделить их на пять классов.
Класс 1: Флаг navigator.webdriver
Самый известный вектор детекции. Когда Chromium запускается с включённой автоматизацией, navigator.webdriver возвращает true. Спецификация WebDriver (W3C) предписывает это поведение.
// Проверка детекции
if (navigator.webdriver === true) {
reportBot();
}
Типичный «фикс»: Переопределение через CDP:
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => false,
});
});
Почему не работает: Это переопределение выполняется в JavaScript-контексте страницы, но антидетект-скрипты могут проверить само переопределение:
// Детекция переопределения
const descriptor = Object.getOwnPropertyDescriptor(navigator, 'webdriver');
if (descriptor && descriptor.get && descriptor.get.toString().includes('false')) {
reportBot(); // Свойство было переопределено геттером
}
// Проверка цепочки прототипов
if (navigator.webdriver !== Navigator.prototype.webdriver) {
reportBot(); // Несовпадение между инстансом и прототипом
}
// Изоляция через iframe
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
if (iframe.contentWindow.navigator.webdriver === true) {
reportBot(); // Переопределение не распространилось на новый контекст
}
Техника с iframe особенно разрушительна: каждый новый browsing context получает свежий объект navigator. Если ваш патч затрагивает только основной фрейм, любой динамически созданный iframe раскроет реальное значение.
Класс 2: CDP-специфичные runtime-объекты
Когда CDP-сессия активна, Chromium экспонирует внутренние объекты, которых нет в обычных экземплярах браузера:
// Runtime.enable создаёт это
window.__cdp_runtime_enabled // внутренний флаг
// Артефакты Page.addScriptToEvaluateOnNewDocument
window.__playwright_evaluation_script_
// Puppeteer-специфичные
window.__puppeteer_utility_world__
Современные скрипты детекции перечисляют все свойства window и сравнивают их с известным чистым профилем браузера:
function detectCDPArtifacts() {
const knownClean = new Set([/* сотни стандартных свойств */]);
const current = Object.getOwnPropertyNames(window);
for (const prop of current) {
if (!knownClean.has(prop)) {
// Неизвестное свойство — потенциальный артефакт автоматизации
logSuspicious(prop);
}
}
// Проверка протоколов отладки
if (window.cdc_adoQpoasnfa76pfcZLmcfl_Array ||
window.cdc_adoQpoasnfa76pfcZLmcfl_Promise ||
window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol) {
reportBot(); // Управляющие переменные ChromeDriver
}
}
Переменные с префиксом cdc_ — это управляющие переменные ChromeDriver, инжектируемые в каждую страницу. Их имена частично рандомизируются для каждой сборки ChromeDriver, но паттерн (cdc_ + случайная строка + _ + имя JS-типа) остаётся стабильным.
Класс 3: Аргументы запуска Chrome
Инструменты автоматизации передают Chromium специфические аргументы командной строки. Эти аргументы оставляют следы, которые JavaScript может обнаружить:
# Типичные аргументы запуска Puppeteer
--disable-blink-features=AutomationControlled
--enable-automation
--remote-debugging-port=9222
--disable-background-networking
--disable-default-apps
--disable-extensions
--disable-sync
--disable-translate
--metrics-recording-only
--no-first-run
Детекция через внутренние API Chrome:
// chrome.runtime модифицируется при --enable-automation
if (chrome.runtime && chrome.runtime.id === undefined &&
chrome.runtime.connect === undefined) {
reportBot(); // Режим автоматизации урезает возможности runtime
}
// Проверка поведения Permissions API
if (!navigator.permissions ||
navigator.permissions.query === undefined) {
reportBot();
}
// Разрешение уведомлений в режиме автоматизации
navigator.permissions.query({ name: 'notifications' }).then(result => {
if (result.state === 'denied' && !wasExplicitlyDenied()) {
reportBot(); // Режим автоматизации автоматически запрещает уведомления
}
});
Класс 4: Детекция headless-режима
Даже с --headless=new (новый headless в Chrome 112+) различия сохраняются:
// Различия строк WebGL-рендерера
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
const renderer = gl.getParameter(gl.RENDERER);
const vendor = gl.getParameter(gl.VENDOR);
// Headless Chrome часто возвращает:
// RENDERER: "SwiftShader" или "Google SwiftShader"
// VENDOR: "Google Inc. (Google)"
// Реальный Chrome возвращает информацию о GPU:
// RENDERER: "ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Direct3D11 ...)"
// VENDOR: "Google Inc. (NVIDIA)"
// Количество плагинов
if (navigator.plugins.length === 0) {
reportBot(); // В headless нет плагинов
}
// Консистентность языков
if (navigator.languages.length === 0 ||
navigator.languages[0] !== navigator.language) {
reportBot();
}
// Размеры экрана в headless
if (window.outerWidth === 0 || window.outerHeight === 0) {
reportBot();
}
Класс 5: Поведенческий анализ
Помимо статических проверок свойств, антифрод-системы анализируют паттерны взаимодействия, характерные для CDP-автоматизации:
// Паттерны движения мыши
// CDP Input.dispatchMouseEvent создаёт синтетические события
// без промежуточных move-событий, которые генерирует реальная мышь
document.addEventListener('mousemove', (e) => {
movements.push({
x: e.clientX, y: e.clientY,
timestamp: e.timeStamp,
isTrusted: e.isTrusted
});
});
// После сбора событий:
function analyzeMovements(events) {
// Реальные люди создают кривые с ускорением
// CDP создаёт прямые линии или без промежуточных точек
for (let i = 1; i < events.length; i++) {
const dt = events[i].timestamp - events[i-1].timestamp;
const dx = events[i].x - events[i-1].x;
const dy = events[i].y - events[i-1].y;
const distance = Math.sqrt(dx*dx + dy*dy);
// Телепортирующаяся мышь (большое расстояние, малое время)
if (distance > 100 && dt < 5) {
scores.teleport++;
}
// Идеально прямые линии (нулевая кривизна)
if (i >= 2) {
const curvature = computeCurvature(
events[i-2], events[i-1], events[i]
);
if (curvature === 0) scores.straight++;
}
}
}
Почему стелс-плагины недостаточны
Пакет puppeteer-extra-plugin-stealth и аналогичные решения пытаются патчить известные векторы детекции в runtime. Вот почему этот подход архитектурно порочен.
Проблема тайминга
Стелс-плагины инжектируют патчи через Page.addScriptToEvaluateOnNewDocument. Эта CDP-команда выполняет JavaScript до скриптов страницы, но после инициализации внутреннего состояния браузера. Антидетект-проверки, запускающиеся в привилегированном контексте браузера (Trusted Types, обработчики нарушений CSP), могут наблюдать оригинальное состояние до применения патчей.
Проблема полноты
Каждое обновление Chromium вводит новые поверхности детекции. Сообщество стелс-плагинов вечно реактивно — патчит векторы после их появления в антифрод-SDK. Индустрия детекции (DataDome, PerimeterX/HUMAN, Kasada, Akamai Bot Manager) содержит штатных исследователей, обнаруживающих новые векторы, в то время как open-source стелс-сообщество латает их неделями или месяцами позже.
Проблема консистентности
Патчинг отдельных свойств создаёт несогласованности. Если вы переопределяете navigator.webdriver, но не патчите соответствующий Blink internal, который его питает, кросс-проверка двух значений выявляет ложь:
// Внутренняя проверка V8
const workerBlob = new Blob([`
postMessage(navigator.webdriver);
`], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(workerBlob));
worker.onmessage = (e) => {
if (e.data !== navigator.webdriver) {
reportBot(); // В worker другое значение webdriver
}
};
Worker’ы выполняются в отдельном контексте. Если ваш патч модифицирует только контекст главного окна, worker’ы раскроют истинное значение.
Глубокий патчинг ядра: единственный жизнеспособный подход
Термин «патчинг ядра» в контексте антидетекта означает модификацию исходного кода браузерного движка на уровне компиляции, а не модификации ядра операционной системы. Именно этот подход используют браузеры вроде Camoufox (на котором основан Santiago) и аналогичные проекты.
Что именно патчится
1. Флаг WebDriver на уровне движка
Вместо переопределения navigator.webdriver через JavaScript, флаг удаляется из IDL-определения браузера:
// В Chromium: third_party/blink/renderer/core/frame/navigator.idl
// ДО:
[RuntimeEnabled=AutomationControlled] readonly attribute boolean webdriver;
// ПОСЛЕ (патч):
// Строка удалена полностью — свойство не существует вообще
В Firefox/Gecko (подход Camoufox):
// dom/webidl/Navigator.webidl
// Атрибут webdriver выкомпилирован из сборки
// Runtime-переопределение не нужно — свойства действительно нет
2. Удаление артефактов CDP
Управляющие переменные вроде cdc_* удаляются из исходного кода драйвера:
// До: ChromeDriver инжектирует управляющие переменные
ExecuteScript("window.cdc_" + randomSuffix + "_Array = Array;");
// После: Код инъекции удалён полностью
// Нечего детектировать, потому что переменные никогда не создаются
3. Флаги автоматизации в бинарнике
Флаг --enable-automation Chromium запускает многочисленные внутренние механизмы. В патченой сборке:
// content/browser/renderer_host/render_process_host_impl.cc
// Фича AutomationControlled отключена на этапе компиляции
// Runtime-проверка этого флага безусловно возвращает false
4. Поверхности детекции HeadlessMode
Патченые браузеры гарантируют, что headless- и headed-режимы создают идентичные отпечатки:
// SwiftShader заменяется реальными данными эмуляции GPU
// Список плагинов заполняется одинаково в обоих режимах
// Метрики экрана возвращают реалистичные значения
Преимущества Firefox/Gecko
Chromium — наиболее изученный браузер для детекции автоматизации; вендоры антифрод инвестируют значительные ресурсы в Chromium-специфичную детекцию. Firefox получает меньше внимания, что является одной из причин, почему Camoufox (и, соответственно, Santiago) выбрал Gecko:
- Меньшая поверхность детекции: В базах антифрод-вендоров меньше известных артефактов автоматизации Gecko
- Другая архитектура: Многопроцессная модель Gecko отличается от модели Chromium, поэтому Chromium-специфичные скрипты детекции дают false positive на Firefox
- Marionette vs CDP: Нативный протокол автоматизации Firefox (Marionette) оставляет другие следы, чем CDP, а WebDriver BiDi (преемник) ещё чище
- Меньше исследователей: Экономика антифрод-исследований благоприятствует анализу Chromium, потому что большинство трафика автоматизации использует Chromium
Детекция за пределами браузера
Даже при идеальной маскировке на уровне браузера, CDP-автоматизация может быть обнаружена через инфраструктурные сигналы:
Паттерны подключений
Реальный пользователь: DNS → TCP → TLS → HTTP (последовательно, с естественными задержками)
CDP-бот: Переиспользует пул соединений, параллельные запросы, нет простоя
Антифрод-системы мониторят поведение на уровне соединений: как быстро запросы следуют за загрузкой страницы, следуют ли запросы подресурсов естественному порядку зависимостей, есть ли в соединении периоды простоя, согласующиеся с временем чтения.
WebSocket порт отладки
CDP коммуницирует через WebSocket. Даже если порт отладки не экспонирован наружу, сканирование локальных портов из JavaScript может его обнаружить:
// Сканирование портов через тайминг WebSocket
async function checkPort(port) {
const start = performance.now();
try {
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
ws.onerror = () => {};
await new Promise(r => setTimeout(r, 100));
ws.close();
} catch (e) {}
const elapsed = performance.now() - start;
// Открытый порт: быстрое подключение или быстрая ошибка
// Закрытый порт: более медленный таймаут
return elapsed < 50;
}
// Проверка типичных портов отладки
for (const port of [9222, 9229, 9333, 0]) {
if (await checkPort(port)) {
reportBot();
}
}
Сигнатуры памяти и производительности
CDP-операции оставляют следы в метриках производительности браузера:
// Автоматизация раздувает определённые performance entries
const entries = performance.getEntriesByType('resource');
const cdpRequests = entries.filter(e =>
e.name.includes('devtools') ||
e.name.includes('inspector')
);
// Давление на память от overhead CDP
if (performance.memory) {
const ratio = performance.memory.usedJSHeapSize /
performance.memory.totalJSHeapSize;
// CDP-сессии увеличивают базовое потребление памяти
if (ratio > 0.9 && performance.memory.jsHeapSizeLimit < 2e9) {
logSuspicious('memory_pressure');
}
}
Подход Santiago: стелс на уровне протокола
Архитектура Santiago полностью избегает проблемы детекции CDP благодаря выбору браузерного движка и протокола автоматизации:
1. Движок Gecko: Используя Firefox (через Camoufox), Santiago работает вне основной экосистемы детекции CDP. Подавляющее большинство антифрод-скриптов оптимизировано для Chromium.
2. Модификации на уровне исходного кода: Все индикаторы автоматизации удалены на уровне исходного кода до компиляции. Runtime-патчинг отсутствует — артефакты автоматизации просто не существуют в бинарнике.
3. Нативная изоляция профилей: Каждый браузерный профиль работает как независимый инстанс со своим состоянием, cookies, local storage и отпечатком. Нет общей CDP-сессии, мультиплексирующей несколько профилей.
4. Нет экспонированного интерфейса отладки: Интерфейс автоматизации — не стандартный протокол отладки, а внутренний IPC-механизм, не создающий видимых сетевых эндпоинтов.
Практическая верификация: тестирование вашей конфигурации
Если вам нужно проверить, детектируема ли ваша текущая автоматизация, используйте эти ресурсы:
Онлайн-тесты детекции
- bot.sannysoft.com — комплексный тест детекции автоматизации
- browserleaks.com — полный анализ отпечатков, включая артефакты CDP
- abrahamjuliot.github.io/creepjs — CreepJS, один из наиболее агрессивных инструментов фингерпринтинга
Скрипт ручной верификации
// Запустите это в консоли вашего автоматизированного браузера
const checks = {
webdriver: navigator.webdriver,
webdriverProto: Navigator.prototype.webdriver,
plugins: navigator.plugins.length,
languages: navigator.languages.length,
chrome: !!window.chrome,
permissions: !!navigator.permissions,
webgl: (() => {
const c = document.createElement('canvas');
const gl = c.getContext('webgl');
return gl ? gl.getParameter(gl.RENDERER) : 'unavailable';
})(),
cdcVars: Object.getOwnPropertyNames(window)
.filter(p => p.startsWith('cdc_')),
domAutomation: !!window.domAutomation,
domAutomationController: !!window.domAutomationController,
};
console.table(checks);
Заключение
Детекция CDP-автоматизации — это не игра в кошки-мышки с равными шансами. У стороны детекции структурные преимущества: они контролируют среду выполнения JavaScript, могут обновлять скрипты детекции серверно за секунды и извлекают выгоду из коллективного интеллекта тысяч сайтов, использующих один и тот же антифрод-SDK.
Единственная устойчивая защита — не быть детектируемым в принципе. Это означает либо использование браузерного движка с удалёнными на уровне исходного кода следами автоматизации, либо принятие факта, что ваш CDP-стек на Playwright/Puppeteer будет отмечен на любом сайте с современной антифрод-защитой. Промежуточного варианта нет — стелс-плагины это временная заплатка поверх перманентного архитектурного дефекта.
Готовы защитить свою цифровую личность?
Выберите тариф и запускайте незаметные профили уже сегодня.
Получайте 15% пожизненную комиссию с каждого реферала.
Стать партнёром →