Як насправді працює виявлення Puppeteer: 12 сигналів, що видають ботів
Фреймворки автоматизації успадковують відбиток реального браузера, але змінюють його десятками помітних способів. Практичний довідник із 12 сигналів JavaScript, мережі та поведінки, якими захисники ловлять Puppeteer і Playwright.
Фреймворки автоматизації керують реальними браузерами, а отже успадковують їхній відбиток. Chrome під керуванням Puppeteer має той самий user agent, ту саму версію Chrome і ту саму збірку Chromium, що й Chrome у людини. Зовні вони виглядають однаково.
Але фреймворки автоматизації змінюють браузер десятками ледь помітних способів — і більшість із них можна спостерігати з JavaScript. Виявлення — не магія. Це контрольний список.
1. Прапорець navigator.webdriver
Найпростіший і найвідоміший сигнал. Коли Chrome працює під фреймворком автоматизації, navigator.webdriver повертає true. Це частина специфікації W3C WebDriver, і Chromium реалізує її за замовчуванням.
Кожен серйозний проєкт автоматизації латає це за лічені секунди. Puppeteer-extra-stealth перевизначає геттер. Користувачі Playwright інʼєктують скрипти Object.defineProperty у нові документи. Приховати прапорець тривіально.
Але сам факт, що його можна тривіально приховати, — вже сигнал. Легітимному Chrome нема чого приховувати. Коли сторінка перевіряє Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver') і дескриптор виглядає інакше, ніж у нативного браузера, автоматизацію викрито навіть після латання прапорця.
2. Аномалія обʼєкта chrome
Легітимний Chrome виставляє глобальний обʼєкт window.chrome зі значною структурою — chrome.runtime, chrome.loadTimes, chrome.csi. Headless Chrome і старіші конфігурації Puppeteer або зовсім не мають цього обʼєкта, або виставляють урізану версію.
Stealth-плагіни відтворюють обʼєкт, але відтворення недосконале. chrome.runtime.onConnect може бути присутнім без chrome.runtime.PlatformOs. Сигнатури функцій можуть повертати обʼєкти замість undefined. Кожна розбіжність — позитивний сигнал.
3. Неузгодженості Permissions API
Реальні браузери повертають узгоджені результати при запиті через Permissions API. navigator.permissions.query({name: 'notifications'}) має повертати 'default', якщо користувач явно не надав і не заборонив дозвіл.
Headless Chrome за замовчуванням повертає 'denied', бо немає UI, щоб показати запит на дозвіл. Фреймворки автоматизації латають це — але часто некоректно. Поширена підказка: запит незвичного дозволу на кшталт 'clipboard-read' повертає результат, що не збігається з поведінкою реального Chrome у поточній версії.
4. Масиви плагінів і MIME-типів
navigator.plugins у реальному браузері повертає PluginArray із записами на кшталт переглядача PDF і Chromium PDF plugin. У headless-режимі цей масив порожній.
Stealth-плагіни додають фейкові записи, але записи часто мають хибні властивості — відсутній length, неправильні поля опису або обʼєкти плагінів, що не поводяться як екземпляри Plugin при перевірці через instanceof.
5. Розбіжності мови та локалі
navigator.language і navigator.languages мають збігатися з HTTP-заголовком Accept-Language. Вони також мають бути узгоджені з часовим поясом, який повідомляє Intl.DateTimeFormat().resolvedOptions().timeZone.
Бот, що видає себе за користувача з Києва (часовий пояс Europe/Kyiv), але надсилає Accept-Language: en-US,en;q=0.9 і повідомляє navigator.language === 'en-US', — можливий, але статистично незвичний. У поєднанні з IP із дата-центру у Франкфурті картина стає очевидною.
6. Рядки WebGL renderer
WebGL2RenderingContext.getParameter(WebGLDebugRendererInfo.UNMASKED_RENDERER_WEBGL) повертає виробника й модель GPU. На реальній машині це може бути:
ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0)
Headless Chrome у контейнері часто повертає:
ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)
Будь-який рядок GPU, що містить SwiftShader або Subzero, — потужний сигнал бота.
7. Аномалії екрана та viewport
Реальне вікно браузера має window.outerWidth і window.outerHeight, що враховують хром браузера — адресний рядок, вкладки, панель закладок. Різниця між зовнішніми та внутрішніми розмірами зазвичай становить 80–140 пікселів по вертикалі.
Headless-браузери часто мають outerHeight === innerHeight, бо немає хрому для відображення. Puppeteer із headless: false виправляє це, але багато конфігурацій автоматизації досі працюють з однаковими розмірами.
8. Перелік шрифтів
Реальні користувачі мають сотні встановлених шрифтів, що різняться залежно від ОС, мови та встановлених застосунків. Windows 11 зі встановленим Office має 500+ шрифтів. Контейнери автоматизації на Alpine Linux мають 30.
Перелік шрифтів через document.fonts.check() або рендеринг у offscreen canvas виявляє це негайно. Браузер, що видає себе за Chrome на Windows 11, але виставляє лише 30 шрифтів, майже напевно є автоматизацією.
9. Ентропія руху миші
Реальні користувачі рухають мишею з дрижанням. Криві нелінійні. Швидкість змінюється. Між двома кліками зазвичай відбуваються сотні подій mousemove.
Фреймворки автоматизації синтезують рух — або прямими лініями до координат, або запрограмованими кривими, яким бракує мікротремору людської руки. Навіть симуляції на кривих Безьє схильні мати підозріло гладкі профілі прискорення.
Системи виявлення ботів збирають траси руху й оцінюють їх за моделлю людського руху. Траси із занадто сталою швидкістю, занадто чистими кривими або відсутніми мікроподіями позначаються.
10. Сигнатури таймингу
Фреймворки автоматизації виконують JavaScript з іншими характеристиками таймингу, ніж браузери під керуванням користувача. Тайминг performance.now() між подіями, особливо між pointerdown і pointerup, має інший розподіл.
Реальний час утримання кліка становить 50–150 мс з високою варіативністю. Час утримання кліка за замовчуванням у Puppeteer — фіксоване значення, часто 30 мс або 100 мс, — майже без варіативності. Сотня кліків з ідентичним часом утримання — беззаперечна автоматизація.
11. Розбіжність TLS-відбитка
Цей сигнал повністю обходить JavaScript. Коли Puppeteer підключається, він використовує TLS-стек Chromium — той самий, що й реальний Chrome. Але скрапери на основі undici чи axios, що видають себе за Chrome через підробку user agent, використовують TLS-стек Node.js, який має виразно інакшу сигнатуру Client Hello (хеш JA4).
Запит, що заявляє User-Agent: Chrome/124.0.6367.60, але має TLS-відбиток Node.js, — миттєве підтвердження небраузерного клієнта.
12. Витік протоколу CDP
Puppeteer і Playwright спілкуються з Chrome через Chrome DevTools Protocol. У деяких конфігураціях цей протокол лишає помітні артефакти — додаткові властивості на обʼєкті Runtime, змінені формати Error.stack або специфічний вивід console.debug, що зʼявляється лише коли підключено CDP-клієнт.
Зокрема, команда Runtime.enable змінює те, як рендеряться трасування стеку. Боти, що ніколи не викликають помилок, уникають цього сигналу, але будь-який потік автоматизації, що натрапляє на виняток (а більшість натрапляє), лишає відбитки.
Нашарування цих сигналів
Жоден окремий сигнал не є доказом автоматизації. Реальний користувач із незвичним драйвером GPU може повертати дивний рядок WebGL. Користувач, орієнтований на приватність, може мати змінений navigator.plugins.
Сила сучасного виявлення — у поєднанні сигналів: 12 слабких спрацювань сильніші за одне сильне, бо реальний користувач рідко зачіпає більш ніж 2–3 аномалії одночасно.
Системи виявлення призначають кожному сигналу вагу й обчислюють бал. Понад порогом відвідувача трактують як автоматизацію. Точний поріг і ваги — найважче налаштувати правильно: занадто суворо — і легітимних користувачів блокують; занадто мʼяко — і витончені боти прослизають.
Самі сигнали — легка частина. Майже кожен фреймворк автоматизації лишає всі 12. Справжня інженерія — вирішити, які комбінації важливі, які є хибними спрацюваннями і як оновлювати модель у міру адаптації фреймворків автоматизації.