CDP-утечки в Puppeteer: как антифрод определяет автоматизацию через Chrome DevTools Protocol

CDP-утечки в Puppeteer: как антифрод определяет автоматизацию через Chrome DevTools Protocol
Markus_automation
Markus_automation

Expert in data parsing and automation

Среди инструментов автоматизации Puppeteer исторически занимает особое место. В отличие от тяжеловесного Selenium, он предложил разработчикам нативный и быстрый контроль над Chromium из коробки в среде Node.js.

Puppeteer стал стандартом индустрии благодаря колоссальной экосистеме готовых плагинов и глубокой интеграции с движком V8. Вы можете взять Puppeteer, подключить к нему puppeteer-extra-plugin-stealth, купить качественные прокси, аккуратно подменить отпечатки Canvas и WebGL — и для многих задач этого будет достаточно.

Но на хорошо защищенных сайтах подобный парсер может столкнуться с проблемами. Системы защиты вроде Cloudflare, Akamai или DataDome начнут блокировать ваши сессии. Почему это происходит? 

Проблема кроется не в логике вашего кода или качестве прокси. Причина заложена в самом фундаменте, который делает Puppeteer таким мощным и удобным — в Chrome DevTools Protocol (CDP). Этот низкоуровневый протокол управления оставляет специфические цифровые следы, которые современные антифрод-алгоритмы умеют читать и учитывать при выявлении ботов. 

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

Содержание

Сохраняйте анонимность с Octo Browser, ведь отследить ваш реальный цифровой отпечаток невозможно.

Что такое CDP и почему он оставляет цифровые следы

Chrome DevTools Protocol предоставляет низкоуровневый доступ к архитектуре браузера. Изначально он создавался для отладки, инспектирования и профилирования, но не для скрытного парсинга.

Протокол функционирует через WebSocket-соединения, напрямую взаимодействуя с движком V8. Когда ваш скрипт отправляет команду (например, клик или переход по ссылке), браузер открывает локальный сокет для двустороннего обмена данными. Этот процесс неизбежно формирует внутренние логи, меняет распределение памяти и оставляет артефакты в изолированной среде браузера. Защитные механизмы способны анализировать эти микроизменения и тайминги для выявления машинного поведения. Следовательно, сам факт поддержания соединения уже демаскирует вашу автоматизацию.

В чем отличие CDP-утечек от стандартного фингерпринтинга

Важно понимать, что CDP-утечки и утечки через отпечатки браузера — это два принципиально разных вектора обнаружения.

  • Классический отпечаток (Canvas, WebGL, Fonts): идентифицирует уникальное оборудование и особенности рендеринга видеокарты. Несовершенство отпечатка или рассинхрон данных позволяют антибот-системам выявлять подмену и маркировать пользователя как бота. 

  • CDP-утечки: раскрывают поведенческие и структурные маркеры исполнения кода. Они показывают сам факт удаленного управления браузером.

У вас может быть уникальный отпечаток WebGL, но флаги автоматизации сведут эту уникальность к нулю. Если проверка отпечатка браузера требует ресурсозатратных операций и времени, то проверка переменных среды или стека вызовов не стоит практически ничего. Именно поэтому антифрод-системам выгодно использовать CDP-детекты. Они позволяют отсекать ботов на этапе базовых протокольных проверок, экономя серверные мощности на глубоком поведенческом анализе.

Анатомия утечек через Chrome DevTools Protocol

Маркеры контекста исполнения (Execution Context) 

Главная слабость Puppeteer в том, как именно он заставляет браузер выполнять ваш код. Под капотом метод page.evaluate() использует CDP-команду Runtime.evaluate. 

При передаче скрипта в браузер современные версии Puppeteer формируют специфический Source URL, который привязывается к этому куску кода. Если в процессе выполнения скрипта возникает ошибка, движок V8 формирует стандартный стек вызовов, и там появляется примерно следующее:

at pptr:evaluate;C:\Users\Admin\Projects\...\main.js:17:14
at pptr:evaluate;C:\Users\Admin\Projects\...\main.js:17:14

Антифрод, переопределяя базовые функции, проактивно провоцирует невидимые ошибки и читает объект Error.stack. Он видит там: 

  • Префикс pptr: — прямую аббревиатуру Puppeteer. 

  • Абсолютный путь к файлу на вашем локальном диске или сервере (начинающийся с C:\ или /var/www/...).

Браузер реального пользователя, находясь на публичном сайте, никогда не исполняет код с локальных путей файловой системы, соответственно, подобные флаги — это стопроцентный маркер автоматизации.

Маркеры контекста исполнения (Execution Context) 

Эту проблему не решает смена IP-адреса или подмена Canvas. Чтобы скрыть этот маркер, вам придется вмешаться либо в исходники библиотеки, либо в среду исполнения браузера.

Самый надежный способ — вырезать маркер pptr:evaluate из исходного кода библиотеки Puppeteer до того, как она отправит команду в браузер. Искать и править файлы руками после каждого npm install нерационально, поэтому лучше написать простой патчер:

const fs = require('fs');
const path = require('path');

// Путь к файлу ExecutionContext.js в новых версиях Puppeteer
const targetFile = path.resolve(__dirname, 'node_modules/puppeteer-core/lib/cjs/puppeteer/cdp/ExecutionContext.js');

if (fs.existsSync(targetFile)) {
    let content = fs.readFileSync(targetFile, 'utf8');
    // Заменяем префикс pptr:evaluate на анонимный вызов
    // и обрезаем локальный путь к файлу
    const patchedContent = content.replace(/pptr:evaluate;.*?\\n/g, 'anonymous:evaluation;\n');
    fs.writeFileSync(targetFile, patchedContent, 'utf8');
    console.log('Puppeteer пропатчен! Маркеры pptr:evaluate удалены.');
}
const fs = require('fs');
const path = require('path');

// Путь к файлу ExecutionContext.js в новых версиях Puppeteer
const targetFile = path.resolve(__dirname, 'node_modules/puppeteer-core/lib/cjs/puppeteer/cdp/ExecutionContext.js');

if (fs.existsSync(targetFile)) {
    let content = fs.readFileSync(targetFile, 'utf8');
    // Заменяем префикс pptr:evaluate на анонимный вызов
    // и обрезаем локальный путь к файлу
    const patchedContent = content.replace(/pptr:evaluate;.*?\\n/g, 'anonymous:evaluation;\n');
    fs.writeFileSync(targetFile, patchedContent, 'utf8');
    console.log('Puppeteer пропатчен! Маркеры pptr:evaluate удалены.');
}

Если нет возможности исправить node_modules, маркеры можно вырезать на лету, внедрив маскировочный скрипт до загрузки страницы сайта. Он переопределяет базовое поведение объекта Error и фильтрует стек вызовов:

await page.evaluateOnNewDocument(() => {
    // Сохраняем оригинальный класс Error
    const NativeError = window.Error;

    window.Error = function(...args) {
        const err = new NativeError(...args);
        const originalStack = err.stack;

        if (originalStack) {
            Object.defineProperty(err, 'stack', {
                get: function() {
                    // Разбиваем стек на строки и вырезаем маркер Puppeteer
                    return originalStack
                        .split('\n')
                        .filter(line => !line.includes('pptr:evaluate'))
                        .join('\n');
                }
            });
        }
        return err;
    };
    
    // Восстанавливаем цепочку прототипов, чтобы она не выдала нас на проверке
    window.Error.prototype = NativeError.prototype;
});
await page.evaluateOnNewDocument(() => {
    // Сохраняем оригинальный класс Error
    const NativeError = window.Error;

    window.Error = function(...args) {
        const err = new NativeError(...args);
        const originalStack = err.stack;

        if (originalStack) {
            Object.defineProperty(err, 'stack', {
                get: function() {
                    // Разбиваем стек на строки и вырезаем маркер Puppeteer
                    return originalStack
                        .split('\n')
                        .filter(line => !line.includes('pptr:evaluate'))
                        .join('\n');
                }
            });
        }
        return err;
    };
    
    // Восстанавливаем цепочку прототипов, чтобы она не выдала нас на проверке
    window.Error.prototype = NativeError.prototype;
});
В итоге вместо локальных путей будет выводиться примерно такой текст

В итоге вместо локальных путей будет выводиться примерно такой текст

Уязвимости метода Page.addScriptToEvaluateOnNewDocument

Попытки скрыть автоматизацию (включая использование популярных stealth-плагинов) чаще всего сводятся к инъекции маскировочного JavaScript до загрузки страницы целевого сайта. В Puppeteer это делается через команду Page.addScriptToEvaluateOnNewDocument.

Сам факт использования этого метода оставляет четкие следы в среде исполнения.

  1. Аномалии таймингов: команда заставляет движок V8 синхронно выполнить ваш код в момент создания контекста страницы, до того как HTML-парсер начнет свою работу. Внедрение объемных скриптов создает ощутимые микрозадержки на этапе document_start. Антифрод измеряет время между внутренними событиями браузера и видит неестественный временной провал.

  2. Нарушение жизненного цикла: stealth-плагины не могут просто стереть флаги автоматизации. Они используют сложные перехватчики. Защитный механизм выявляет, что сложные прокси-объекты и переопределенные свойства появились в объекте window аномально рано — до события DOMContentLoaded или даже до парсинга тега <head>. Это нарушает естественный жизненный цикл страницы. 

  3. Отсутствие изоляции: любые скрипты, внедренные через CDP-команду addScriptToEvaluateOnNewDocument, выполняются в главном контексте страницы. Это значит, что код вашего маскировочного плагина и скрипты антифрод-системы сайта оказываются в одном пространстве. Малейший баг или случайно оставленная переменная — и защита сайта их увидит. 

Отказаться от маскировки нельзя, но можно изменить способ доставки маскировочного кода. Вместо того чтобы вбрасывать код через протокол отладки, можно использовать нативный механизм самого браузера — загрузку собственного расширения.

Браузеры проектировались так, чтобы расширения могли безопасно внедрять код. Если упаковать логику маскировки в расширение Manifest V3 и загрузить его при старте Puppeteer, антифрод воспримет эти тайминги и инъекции как работу обычного пользовательского аддона.

Например, вы можете создать папку stealth-extension с файлом manifest.json:

{
  "manifest_version": 3,
  "name": "My Custom Stealth",
  "version": "1.0",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["inject.js"],
      "run_at": "document_start",
      "world": "MAIN"
    }
  ]
}
{
  "manifest_version": 3,
  "name": "My Custom Stealth",
  "version": "1.0",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["inject.js"],
      "run_at": "document_start",
      "world": "MAIN"
    }
  ]
}

И подключить его при запуске Puppeteer вместо вызова evaluateOnNewDocument:

const browser = await puppeteer.launch({
  args: [
    `--disable-extensions-except=${pathToExtension}`,
    `--load-extension=${pathToExtension}`
  ]
});
const browser = await puppeteer.launch({
  args: [
    `--disable-extensions-except=${pathToExtension}`,
    `--load-extension=${pathToExtension}`
  ]
});

Второй способ — вообще не использовать механизмы V8 для инъекции. Вы можете использовать внешний прокси-сервер или встроенный интерцептор запросов Puppeteer, чтобы модифицировать сырой HTML-ответ от сервера на лету.

Вставьте свой тег <script> с маскировкой в самую первую строку тега <head>. В этом случае код выполнится абсолютно естественным образом в рамках стандартного парсинга документа, не вызывая подозрений у счетчиков таймингов.

Если нужно обязательно использовать addScriptToEvaluateOnNewDocument, откажитесь от загрузки монолитных stealth-плагинов. Разделите маскировку на два этапа. 

Перед загрузкой страницы избавьтесь только от маркера webdriver. Этот код выполняется за доли миллисекунды и не вызывает аномалий в Performance API.

// Внедряем микропейлоад до парсинга HTML
await page.evaluateOnNewDocument(() => {
    // Снимаем только самую главную метку бота
    Object.defineProperty(navigator, 'webdriver', {
        get: () => undefined,
    });
    // Никаких тяжелых подмен WebGL или Canvas здесь нет!
});
// Внедряем микропейлоад до парсинга HTML
await page.evaluateOnNewDocument(() => {
    // Снимаем только самую главную метку бота
    Object.defineProperty(navigator, 'webdriver', {
        get: () => undefined,
    });
    // Никаких тяжелых подмен WebGL или Canvas здесь нет!
});

Всю остальную логику (подмену отпечатков видеокарты, аудио, плагинов) грузите позже, когда браузер уже начал отрисовывать страницу. Это можно сделать через стандартный page.evaluate или привязав к событию DOMContentLoaded

// Идем на сайт
await page.goto('https://target-site.com');

// Страница уже грузится, тайминги антифрода пройдены. 
// Теперь можно безопасно внедрять тяжелую маскировку
await page.evaluate(() => {
    // Здесь подменяем Canvas, WebGL, шрифты и прочее
    const getParameter = WebGLRenderingContext.getParameter;
    WebGLRenderingContext.prototype.getParameter = function(parameter) {
        if (parameter === 37445) return 'Intel Inc.';
        if (parameter === 37446) return 'Intel Iris OpenGL Engine';
        return getParameter(parameter);
    };
});
// Идем на сайт
await page.goto('https://target-site.com');

// Страница уже грузится, тайминги антифрода пройдены. 
// Теперь можно безопасно внедрять тяжелую маскировку
await page.evaluate(() => {
    // Здесь подменяем Canvas, WebGL, шрифты и прочее
    const getParameter = WebGLRenderingContext.getParameter;
    WebGLRenderingContext.prototype.getParameter = function(parameter) {
        if (parameter === 37445) return 'Intel Inc.';
        if (parameter === 37446) return 'Intel Iris OpenGL Engine';
        return getParameter(parameter);
    };
});

Системы защиты, как правило, проверяют webdriver синхронно в самом начале — и такой способ успешно проходит подобную проверку. А более глубокие тесты антифрод запускает асинхронно, уже в процессе работы со страницей. К этому моменту второй, более тяжелый скрипт уже успеет загрузиться и подменить нужные функции, не создав задержек в начале жизненного цикла страницы. 

Проблема Network.setUserAgentOverride

Сменить User-Agent через функционал CDP легко, но проблема в том, что так меняется только HTTP-заголовок, а другие маркеры среды остаются нетронутыми. 

А значит, вы получаете критический рассинхрон. Метод Network.setUserAgentOverride не способен корректно подменить внутренние свойства объекта navigator или специфические заголовки Client Hints. Аналитическая система видит, к примеру, мобильный User-Agent в заголовке, но фиксирует десктопное поведение API-рендеринга и несовпадающие заголовки  sec-ch-ua.

Никогда не меняйте только строчку User-Agent. Если вы беретесь за эмуляцию, вам нужно подделывать весь комплекс Client Hints и аппаратных сенсоров. Вместо setUserAgent используйте расширенные параметры CDP, передавая объект userAgentMetadata, и не забывайте о специфичных паттернах, характерных для мобильных устройств (как минимум эмуляция тачскрина).

Сканирование TCP-портов отладки

Чтобы Puppeteer мог управлять браузером, он запускает Chromium с открытым портом для WebSocket-соединения. По умолчанию это часто локальный порт отладки (классический 9222 или любой другой случайный порт).

Антифрод-скрипт загружается прямо в браузер пользователя. И уже от имени вашего браузера, находясь внутри вашей системы, он делает банальный AJAX-запрос по адресу http://127.0.0.1:9222/json/version.

Защита использует так называемые тайминг-атаки или сканирование локальной сети через WebSocket. Если скрипт стучится в стандартный порт отладки и получает мгновенный ответ от движка — он понимает, что браузером сейчас управляет скрипт.

Можно пытаться менять порты на случайные, но сканеры умеют простукивать целые диапазоны. Самое простое решение — вообще отказаться от использования TCP-портов для связи между Node.js и браузером.

Puppeteer умеет общаться с Chromium не через сетевые сокеты, а через анонимные каналы связи операционной системы. В этом режиме порты отладки вообще не открываются, и сканировать антифроду нечего.

Просто добавьте один флаг pipe: true при запуске вашего браузера. Это защитит вас от сетевого сканирования локального хоста. 

Как добиться результата без детекта

Абсолютная невидимость Puppeteer невозможна в принципе, так как любая эмуляция оставляет отклонения. С технической точки зрения так и есть, в чем мы уже убедились выше.

Базовых плагинов по умолчанию недостаточно для серьезных задач. Если вам нужна высокая анонимность, придется менять подход:

  1. Патчинг бинарного файла Chrome. Это физическая модификация исполняемого файла браузера (например, в hex-редакторе). Цель — заменить жестко закодированные строки протокола на случайные символы в самом исполняемом файле. В отличие от Stealth-плагинов, которые оставляют микроскопическое окно уязвимости между запуском и внедрением JS, бинарная модификация полностью убирает проблему.

  2. Использование специализированных движков. Переход на решения архитектурного уровня (антидетект-браузер), где подмена отпечатков и скрытие автоматизации происходят на уровне кода C++ движка Blink, а не через JS-инъекции.

  3. Отказ от CDP. Перенос логики управления в кастомные расширения Chrome, которые общаются с сервером управления по своим WebSocket-каналам, позволяя закрыть порты отладки.

Использование антидетект-браузеров

Отдельно стоит отметить использование специализированных инструментов, например антидетект-браузеров. Они подходят к задаче через внесение изменений в исходный код Chromium и работу напрямую в ядре. Естественно, очевидные маркеры вроде navigator.webdriver просто физически вырезаются еще на этапе компиляции бинарного файла.

Вся тяжелая работа по эмуляции среды происходит под капотом. Когда скрипт защиты запрашивает параметры видеокарты (WebGL vendor/renderer), движку не нужно запускать JS-обертку для подмены — код C++ нативно и моментально отдает нужные значения. То же самое касается сложных метрик. А пользователь и вовсе не касается всего вышеперечисленного, разве что на уровне настройки нужных параметров перед запуском профиля.

С точки зрения антифрода, такой браузер выглядит как обычный пользователь. А вы можете управлять отпечатком через Puppeteer, просто подключив его к открытому порту антидетекта.

Неочевидные детали и вывод

  • Использование флага --disable-blink-features=AutomationControlled снимает базовую метку webdriver, но оставляет косвенные следы.

  • Принудительное отключение Log.enable и Performance.enable через протокол лишает ваш скрипт части метрик, но повышает общую скрытность.

  • Новый Headless Mode (--headless=new) объединяет архитектуры обычного и headless-браузера, что меняет структуру детектирования ClientRects и шрифтов, делая рендеринг более естественным.

Успешный обход защиты начинается не с установки сотни npm-пакетов, а с понимания того, как работает движок под капотом.

Работа с автоматизацией сегодня требует инженерной педантичности. CDP-утечки — это не баг, а конструктивная особенность инструмента. Изучайте браузер глубже, контролируйте каждый исполняемый контекст, и тогда ваши системы станут по-настоящему устойчивыми.

Сохраняйте анонимность с Octo Browser, ведь отследить ваш реальный цифровой отпечаток невозможно.

Что такое CDP и почему он оставляет цифровые следы

Chrome DevTools Protocol предоставляет низкоуровневый доступ к архитектуре браузера. Изначально он создавался для отладки, инспектирования и профилирования, но не для скрытного парсинга.

Протокол функционирует через WebSocket-соединения, напрямую взаимодействуя с движком V8. Когда ваш скрипт отправляет команду (например, клик или переход по ссылке), браузер открывает локальный сокет для двустороннего обмена данными. Этот процесс неизбежно формирует внутренние логи, меняет распределение памяти и оставляет артефакты в изолированной среде браузера. Защитные механизмы способны анализировать эти микроизменения и тайминги для выявления машинного поведения. Следовательно, сам факт поддержания соединения уже демаскирует вашу автоматизацию.

В чем отличие CDP-утечек от стандартного фингерпринтинга

Важно понимать, что CDP-утечки и утечки через отпечатки браузера — это два принципиально разных вектора обнаружения.

  • Классический отпечаток (Canvas, WebGL, Fonts): идентифицирует уникальное оборудование и особенности рендеринга видеокарты. Несовершенство отпечатка или рассинхрон данных позволяют антибот-системам выявлять подмену и маркировать пользователя как бота. 

  • CDP-утечки: раскрывают поведенческие и структурные маркеры исполнения кода. Они показывают сам факт удаленного управления браузером.

У вас может быть уникальный отпечаток WebGL, но флаги автоматизации сведут эту уникальность к нулю. Если проверка отпечатка браузера требует ресурсозатратных операций и времени, то проверка переменных среды или стека вызовов не стоит практически ничего. Именно поэтому антифрод-системам выгодно использовать CDP-детекты. Они позволяют отсекать ботов на этапе базовых протокольных проверок, экономя серверные мощности на глубоком поведенческом анализе.

Анатомия утечек через Chrome DevTools Protocol

Маркеры контекста исполнения (Execution Context) 

Главная слабость Puppeteer в том, как именно он заставляет браузер выполнять ваш код. Под капотом метод page.evaluate() использует CDP-команду Runtime.evaluate. 

При передаче скрипта в браузер современные версии Puppeteer формируют специфический Source URL, который привязывается к этому куску кода. Если в процессе выполнения скрипта возникает ошибка, движок V8 формирует стандартный стек вызовов, и там появляется примерно следующее:

at pptr:evaluate;C:\Users\Admin\Projects\...\main.js:17:14

Антифрод, переопределяя базовые функции, проактивно провоцирует невидимые ошибки и читает объект Error.stack. Он видит там: 

  • Префикс pptr: — прямую аббревиатуру Puppeteer. 

  • Абсолютный путь к файлу на вашем локальном диске или сервере (начинающийся с C:\ или /var/www/...).

Браузер реального пользователя, находясь на публичном сайте, никогда не исполняет код с локальных путей файловой системы, соответственно, подобные флаги — это стопроцентный маркер автоматизации.

Маркеры контекста исполнения (Execution Context) 

Эту проблему не решает смена IP-адреса или подмена Canvas. Чтобы скрыть этот маркер, вам придется вмешаться либо в исходники библиотеки, либо в среду исполнения браузера.

Самый надежный способ — вырезать маркер pptr:evaluate из исходного кода библиотеки Puppeteer до того, как она отправит команду в браузер. Искать и править файлы руками после каждого npm install нерационально, поэтому лучше написать простой патчер:

const fs = require('fs');
const path = require('path');

// Путь к файлу ExecutionContext.js в новых версиях Puppeteer
const targetFile = path.resolve(__dirname, 'node_modules/puppeteer-core/lib/cjs/puppeteer/cdp/ExecutionContext.js');

if (fs.existsSync(targetFile)) {
    let content = fs.readFileSync(targetFile, 'utf8');
    // Заменяем префикс pptr:evaluate на анонимный вызов
    // и обрезаем локальный путь к файлу
    const patchedContent = content.replace(/pptr:evaluate;.*?\\n/g, 'anonymous:evaluation;\n');
    fs.writeFileSync(targetFile, patchedContent, 'utf8');
    console.log('Puppeteer пропатчен! Маркеры pptr:evaluate удалены.');
}

Если нет возможности исправить node_modules, маркеры можно вырезать на лету, внедрив маскировочный скрипт до загрузки страницы сайта. Он переопределяет базовое поведение объекта Error и фильтрует стек вызовов:

await page.evaluateOnNewDocument(() => {
    // Сохраняем оригинальный класс Error
    const NativeError = window.Error;

    window.Error = function(...args) {
        const err = new NativeError(...args);
        const originalStack = err.stack;

        if (originalStack) {
            Object.defineProperty(err, 'stack', {
                get: function() {
                    // Разбиваем стек на строки и вырезаем маркер Puppeteer
                    return originalStack
                        .split('\n')
                        .filter(line => !line.includes('pptr:evaluate'))
                        .join('\n');
                }
            });
        }
        return err;
    };
    
    // Восстанавливаем цепочку прототипов, чтобы она не выдала нас на проверке
    window.Error.prototype = NativeError.prototype;
});
В итоге вместо локальных путей будет выводиться примерно такой текст

В итоге вместо локальных путей будет выводиться примерно такой текст

Уязвимости метода Page.addScriptToEvaluateOnNewDocument

Попытки скрыть автоматизацию (включая использование популярных stealth-плагинов) чаще всего сводятся к инъекции маскировочного JavaScript до загрузки страницы целевого сайта. В Puppeteer это делается через команду Page.addScriptToEvaluateOnNewDocument.

Сам факт использования этого метода оставляет четкие следы в среде исполнения.

  1. Аномалии таймингов: команда заставляет движок V8 синхронно выполнить ваш код в момент создания контекста страницы, до того как HTML-парсер начнет свою работу. Внедрение объемных скриптов создает ощутимые микрозадержки на этапе document_start. Антифрод измеряет время между внутренними событиями браузера и видит неестественный временной провал.

  2. Нарушение жизненного цикла: stealth-плагины не могут просто стереть флаги автоматизации. Они используют сложные перехватчики. Защитный механизм выявляет, что сложные прокси-объекты и переопределенные свойства появились в объекте window аномально рано — до события DOMContentLoaded или даже до парсинга тега <head>. Это нарушает естественный жизненный цикл страницы. 

  3. Отсутствие изоляции: любые скрипты, внедренные через CDP-команду addScriptToEvaluateOnNewDocument, выполняются в главном контексте страницы. Это значит, что код вашего маскировочного плагина и скрипты антифрод-системы сайта оказываются в одном пространстве. Малейший баг или случайно оставленная переменная — и защита сайта их увидит. 

Отказаться от маскировки нельзя, но можно изменить способ доставки маскировочного кода. Вместо того чтобы вбрасывать код через протокол отладки, можно использовать нативный механизм самого браузера — загрузку собственного расширения.

Браузеры проектировались так, чтобы расширения могли безопасно внедрять код. Если упаковать логику маскировки в расширение Manifest V3 и загрузить его при старте Puppeteer, антифрод воспримет эти тайминги и инъекции как работу обычного пользовательского аддона.

Например, вы можете создать папку stealth-extension с файлом manifest.json:

{
  "manifest_version": 3,
  "name": "My Custom Stealth",
  "version": "1.0",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["inject.js"],
      "run_at": "document_start",
      "world": "MAIN"
    }
  ]
}

И подключить его при запуске Puppeteer вместо вызова evaluateOnNewDocument:

const browser = await puppeteer.launch({
  args: [
    `--disable-extensions-except=${pathToExtension}`,
    `--load-extension=${pathToExtension}`
  ]
});

Второй способ — вообще не использовать механизмы V8 для инъекции. Вы можете использовать внешний прокси-сервер или встроенный интерцептор запросов Puppeteer, чтобы модифицировать сырой HTML-ответ от сервера на лету.

Вставьте свой тег <script> с маскировкой в самую первую строку тега <head>. В этом случае код выполнится абсолютно естественным образом в рамках стандартного парсинга документа, не вызывая подозрений у счетчиков таймингов.

Если нужно обязательно использовать addScriptToEvaluateOnNewDocument, откажитесь от загрузки монолитных stealth-плагинов. Разделите маскировку на два этапа. 

Перед загрузкой страницы избавьтесь только от маркера webdriver. Этот код выполняется за доли миллисекунды и не вызывает аномалий в Performance API.

// Внедряем микропейлоад до парсинга HTML
await page.evaluateOnNewDocument(() => {
    // Снимаем только самую главную метку бота
    Object.defineProperty(navigator, 'webdriver', {
        get: () => undefined,
    });
    // Никаких тяжелых подмен WebGL или Canvas здесь нет!
});

Всю остальную логику (подмену отпечатков видеокарты, аудио, плагинов) грузите позже, когда браузер уже начал отрисовывать страницу. Это можно сделать через стандартный page.evaluate или привязав к событию DOMContentLoaded

// Идем на сайт
await page.goto('https://target-site.com');

// Страница уже грузится, тайминги антифрода пройдены. 
// Теперь можно безопасно внедрять тяжелую маскировку
await page.evaluate(() => {
    // Здесь подменяем Canvas, WebGL, шрифты и прочее
    const getParameter = WebGLRenderingContext.getParameter;
    WebGLRenderingContext.prototype.getParameter = function(parameter) {
        if (parameter === 37445) return 'Intel Inc.';
        if (parameter === 37446) return 'Intel Iris OpenGL Engine';
        return getParameter(parameter);
    };
});

Системы защиты, как правило, проверяют webdriver синхронно в самом начале — и такой способ успешно проходит подобную проверку. А более глубокие тесты антифрод запускает асинхронно, уже в процессе работы со страницей. К этому моменту второй, более тяжелый скрипт уже успеет загрузиться и подменить нужные функции, не создав задержек в начале жизненного цикла страницы. 

Проблема Network.setUserAgentOverride

Сменить User-Agent через функционал CDP легко, но проблема в том, что так меняется только HTTP-заголовок, а другие маркеры среды остаются нетронутыми. 

А значит, вы получаете критический рассинхрон. Метод Network.setUserAgentOverride не способен корректно подменить внутренние свойства объекта navigator или специфические заголовки Client Hints. Аналитическая система видит, к примеру, мобильный User-Agent в заголовке, но фиксирует десктопное поведение API-рендеринга и несовпадающие заголовки  sec-ch-ua.

Никогда не меняйте только строчку User-Agent. Если вы беретесь за эмуляцию, вам нужно подделывать весь комплекс Client Hints и аппаратных сенсоров. Вместо setUserAgent используйте расширенные параметры CDP, передавая объект userAgentMetadata, и не забывайте о специфичных паттернах, характерных для мобильных устройств (как минимум эмуляция тачскрина).

Сканирование TCP-портов отладки

Чтобы Puppeteer мог управлять браузером, он запускает Chromium с открытым портом для WebSocket-соединения. По умолчанию это часто локальный порт отладки (классический 9222 или любой другой случайный порт).

Антифрод-скрипт загружается прямо в браузер пользователя. И уже от имени вашего браузера, находясь внутри вашей системы, он делает банальный AJAX-запрос по адресу http://127.0.0.1:9222/json/version.

Защита использует так называемые тайминг-атаки или сканирование локальной сети через WebSocket. Если скрипт стучится в стандартный порт отладки и получает мгновенный ответ от движка — он понимает, что браузером сейчас управляет скрипт.

Можно пытаться менять порты на случайные, но сканеры умеют простукивать целые диапазоны. Самое простое решение — вообще отказаться от использования TCP-портов для связи между Node.js и браузером.

Puppeteer умеет общаться с Chromium не через сетевые сокеты, а через анонимные каналы связи операционной системы. В этом режиме порты отладки вообще не открываются, и сканировать антифроду нечего.

Просто добавьте один флаг pipe: true при запуске вашего браузера. Это защитит вас от сетевого сканирования локального хоста. 

Как добиться результата без детекта

Абсолютная невидимость Puppeteer невозможна в принципе, так как любая эмуляция оставляет отклонения. С технической точки зрения так и есть, в чем мы уже убедились выше.

Базовых плагинов по умолчанию недостаточно для серьезных задач. Если вам нужна высокая анонимность, придется менять подход:

  1. Патчинг бинарного файла Chrome. Это физическая модификация исполняемого файла браузера (например, в hex-редакторе). Цель — заменить жестко закодированные строки протокола на случайные символы в самом исполняемом файле. В отличие от Stealth-плагинов, которые оставляют микроскопическое окно уязвимости между запуском и внедрением JS, бинарная модификация полностью убирает проблему.

  2. Использование специализированных движков. Переход на решения архитектурного уровня (антидетект-браузер), где подмена отпечатков и скрытие автоматизации происходят на уровне кода C++ движка Blink, а не через JS-инъекции.

  3. Отказ от CDP. Перенос логики управления в кастомные расширения Chrome, которые общаются с сервером управления по своим WebSocket-каналам, позволяя закрыть порты отладки.

Использование антидетект-браузеров

Отдельно стоит отметить использование специализированных инструментов, например антидетект-браузеров. Они подходят к задаче через внесение изменений в исходный код Chromium и работу напрямую в ядре. Естественно, очевидные маркеры вроде navigator.webdriver просто физически вырезаются еще на этапе компиляции бинарного файла.

Вся тяжелая работа по эмуляции среды происходит под капотом. Когда скрипт защиты запрашивает параметры видеокарты (WebGL vendor/renderer), движку не нужно запускать JS-обертку для подмены — код C++ нативно и моментально отдает нужные значения. То же самое касается сложных метрик. А пользователь и вовсе не касается всего вышеперечисленного, разве что на уровне настройки нужных параметров перед запуском профиля.

С точки зрения антифрода, такой браузер выглядит как обычный пользователь. А вы можете управлять отпечатком через Puppeteer, просто подключив его к открытому порту антидетекта.

Неочевидные детали и вывод

  • Использование флага --disable-blink-features=AutomationControlled снимает базовую метку webdriver, но оставляет косвенные следы.

  • Принудительное отключение Log.enable и Performance.enable через протокол лишает ваш скрипт части метрик, но повышает общую скрытность.

  • Новый Headless Mode (--headless=new) объединяет архитектуры обычного и headless-браузера, что меняет структуру детектирования ClientRects и шрифтов, делая рендеринг более естественным.

Успешный обход защиты начинается не с установки сотни npm-пакетов, а с понимания того, как работает движок под капотом.

Работа с автоматизацией сегодня требует инженерной педантичности. CDP-утечки — это не баг, а конструктивная особенность инструмента. Изучайте браузер глубже, контролируйте каждый исполняемый контекст, и тогда ваши системы станут по-настоящему устойчивыми.

Следите за последними новостями Octo Browser

Нажимая кнопку, вы соглашаетесь с нашей политикой конфиденциальности.

Следите за последними новостями Octo Browser

Нажимая кнопку, вы соглашаетесь с нашей политикой конфиденциальности.

Следите за последними новостями Octo Browser

Нажимая кнопку, вы соглашаетесь с нашей политикой конфиденциальности.

Присоединяйтесь к Octo Browser сейчас

Вы можете обращаться за помощью к нашим специалистам службы поддержки в чате в любое время.

Присоединяйтесь к Octo Browser сейчас

Вы можете обращаться за помощью к нашим специалистам службы поддержки в чате в любое время.

Присоединяйтесь к Octo Browser сейчас

Вы можете обращаться за помощью к нашим специалистам службы поддержки в чате в любое время.

©

2026

Octo Browser

©

2026

Octo Browser

©

2026

Octo Browser