Jak naprawdę działa wykrywanie Puppeteera: 12 sygnałów, które zdradzają boty
Frameworki automatyzujące dziedziczą odcisk prawdziwej przeglądarki, ale zmieniają go na dziesiątki obserwowalnych sposobów. Przewodnik po 12 sygnałach JavaScript, sieciowych i behawioralnych, którymi łapie się Puppeteera i Playwrighta.
Frameworki automatyzujące sterują prawdziwymi przeglądarkami, co oznacza, że dziedziczą ich odcisk. Chrome sterowany Puppeteerem ma ten sam user agent, tę samą wersję Chrome i tę samą kompilację Chromium co przeglądarka człowieka. Powierzchownie wyglądają identycznie.
Ale frameworki automatyzujące zmieniają przeglądarkę na dziesiątki subtelnych sposobów — większość z nich da się zaobserwować z poziomu JavaScriptu. Wykrywanie to nie magia. To lista kontrolna.
1. Flaga navigator.webdriver
Najprostszy i najsłynniejszy sygnał. Gdy Chrome działa pod frameworkiem automatyzującym, navigator.webdriver zwraca true. To część specyfikacji W3C WebDriver, a Chromium implementuje ją domyślnie.
Każdy poważny projekt automatyzacji łata to w kilka sekund. Puppeteer-extra-stealth nadpisuje getter. Użytkownicy Playwrighta wstrzykują skrypty Object.defineProperty do nowych dokumentów. Ukrycie flagi jest trywialne.
Ale sam fakt, że można ją trywialnie ukryć, jest sygnałem. Prawdziwy Chrome nie musi niczego ukrywać. Gdy strona sprawdza Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver'), a deskryptor wygląda inaczej niż w natywnej przeglądarce — automatyzacja zostaje przyłapana nawet po załataniu flagi.
2. Anomalia obiektu chrome
Prawdziwy Chrome udostępnia globalny obiekt window.chrome o rozbudowanej strukturze — chrome.runtime, chrome.loadTimes, chrome.csi. Headless Chrome i starsze konfiguracje Puppeteera albo pomijają ten obiekt w całości, albo udostępniają jego okrojoną wersję.
Wtyczki stealth odtwarzają ten obiekt, ale odtworzenie jest niedoskonałe. chrome.runtime.onConnect może być obecne bez chrome.runtime.PlatformOs. Sygnatury funkcji mogą zwracać obiekty zamiast undefined. Każda niezgodność to pozytywny sygnał.
3. Niespójności Permissions API
Prawdziwe przeglądarki zwracają spójne wyniki przy odpytywaniu przez Permissions API. navigator.permissions.query({name: 'notifications'}) powinno zwrócić 'default', jeśli użytkownik nie przyznał ani nie odmówił uprawnienia jawnie.
Headless Chrome zwraca domyślnie 'denied', bo nie ma interfejsu do wyświetlenia monitu o uprawnienia. Frameworki automatyzujące to łatają — ale często niepoprawnie. Typowa oznaka: odpytanie o nietypowe uprawnienie, jak 'clipboard-read', zwraca wynik niezgodny z tym, co robi prawdziwy Chrome w bieżącej wersji.
4. Tablice wtyczek i typów MIME
navigator.plugins w prawdziwej przeglądarce zwraca PluginArray z wpisami takimi jak przeglądarka PDF i wtyczka Chromium PDF. W trybie headless ta tablica jest pusta.
Wtyczki stealth dodają fałszywe wpisy, ale wpisy te często mają błędne właściwości — brakujące length, niewłaściwe pola opisu albo obiekty wtyczek, które nie zachowują się jak instancje Plugin przy teście instanceof.
5. Niezgodności języka i ustawień regionalnych
navigator.language i navigator.languages powinny odpowiadać nagłówkowi HTTP Accept-Language. Powinny też być spójne ze strefą czasową raportowaną przez Intl.DateTimeFormat().resolvedOptions().timeZone.
Bot podający się za obecnego w Kijowie (strefa Europe/Kyiv), ale wysyłający Accept-Language: en-US,en;q=0.9 i raportujący navigator.language === 'en-US', jest możliwy, lecz statystycznie nietypowy. W połączeniu z IP z centrum danych we Frankfurcie obraz staje się jasny.
6. Łańcuchy WebGL renderer
WebGL2RenderingContext.getParameter(WebGLDebugRendererInfo.UNMASKED_RENDERER_WEBGL) zwraca producenta i model GPU. Na prawdziwej maszynie może to być:
ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0)
Headless Chrome działający w kontenerze często zwraca:
ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)
Każdy łańcuch GPU zawierający SwiftShader lub Subzero to silny sygnał bota.
7. Anomalie ekranu i okna widoku
Prawdziwe okno przeglądarki ma window.outerWidth i window.outerHeight uwzględniające elementy interfejsu przeglądarki — pasek adresu, karty, pasek zakładek. Różnica między wymiarami zewnętrznymi a wewnętrznymi wynosi zwykle 80–140 pikseli w pionie.
Przeglądarki headless często mają outerHeight === innerHeight, bo nie ma interfejsu do wyświetlenia. Puppeteer z headless: false to naprawia, ale wiele konfiguracji automatyzacji nadal działa z równymi wymiarami.
8. Wyliczanie czcionek
Prawdziwi użytkownicy mają zainstalowane setki czcionek, różniące się w zależności od systemu, języka i zainstalowanych aplikacji. Windows 11 z zainstalowanym Office ma ponad 500 czcionek. Kontenery automatyzacji działające na Alpine Linux mają 30.
Wyliczanie czcionek przez document.fonts.check() lub renderowanie na canvasie poza ekranem ujawnia to natychmiast. Przeglądarka raportująca się jako Chrome na Windows 11, ale udostępniająca tylko 30 czcionek, to niemal na pewno automatyzacja.
9. Entropia ruchu myszy
Prawdziwi użytkownicy poruszają myszą z drżeniem. Krzywe są nieliniowe. Prędkość się zmienia. Między dwoma kliknięciami występują zwykle setki zdarzeń mousemove.
Frameworki automatyzujące syntetyzują ruch — albo prostymi liniami do współrzędnych, albo zaprogramowanymi krzywymi pozbawionymi mikrodrgań ludzkiej dłoni. Nawet symulacje oparte na krzywych Béziera zwykle mają podejrzanie gładkie profile przyspieszenia.
Systemy wykrywania botów zbierają ślady ruchu i oceniają je względem modelu ludzkiego ruchu. Ślady o zbyt spójnej prędkości, zbyt czystych krzywych lub brakujących mikrozdarzeniach są oznaczane.
10. Sygnatury czasowe
Frameworki automatyzujące wykonują JavaScript o innych charakterystykach czasowych niż przeglądarki sterowane przez użytkownika. Czasy performance.now() między zdarzeniami, zwłaszcza między pointerdown a pointerup, mają inny rozkład.
Prawdziwe czasy przytrzymania kliknięcia to 50–150 ms o dużej wariancji. Domyślne przytrzymanie kliknięcia w Puppeteerze to stała wartość — często 30 ms lub 100 ms — niemal bez wariancji. Sto kliknięć o identycznych czasach przytrzymania to jednoznaczna automatyzacja.
11. Niezgodność odcisku TLS
Ten sygnał całkowicie omija JavaScript. Gdy Puppeteer nawiązuje połączenie, używa stosu TLS Chromium — tak samo jak prawdziwy Chrome. Ale scrapery oparte na undici lub axios, które udają Chrome przez podrobienie user agenta, korzystają ze stosu TLS Node.js, mającego wyraźnie inną sygnaturę Client Hello (hash JA4).
Żądanie deklarujące User-Agent: Chrome/124.0.6367.60, ale prezentujące odcisk TLS Node.js, to natychmiastowe potwierdzenie klienta niebędącego przeglądarką.
12. Wyciek protokołu CDP
Puppeteer i Playwright komunikują się z Chrome przez Chrome DevTools Protocol. W niektórych konfiguracjach protokół ten pozostawia wykrywalne artefakty — dodatkowe właściwości obiektu Runtime, zmienione formaty Error.stack lub konkretne wyjścia console.debug, które pojawiają się tylko przy dołączonym kliencie CDP.
W szczególności polecenie Runtime.enable zmienia sposób renderowania śladów stosu. Boty, które nigdy nie wywołują błędów, unikają tego sygnału, ale każdy przepływ automatyzacji, który natrafi na wyjątek (a większość natrafia), zostawia odciski.
Warstwowanie tych sygnałów
Żaden pojedynczy sygnał nie jest dowodem automatyzacji. Prawdziwy użytkownik z nietypowym sterownikiem GPU może zwrócić dziwny łańcuch WebGL. Użytkownik dbający o prywatność może mieć zmodyfikowane navigator.plugins.
Siła nowoczesnego wykrywania bierze się z łączenia sygnałów — 12 słabych trafień jest silniejszych niż jedno silne trafienie, bo prawdziwy użytkownik rzadko uruchamia jednocześnie więcej niż 2–3 anomalie.
Systemy wykrywania przypisują każdemu sygnałowi wagę i wyliczają wynik. Powyżej progu odwiedzający jest traktowany jako automatyzacja. Dokładny próg i wagi to najtrudniejsze elementy do prawidłowego ustawienia — zbyt surowe i blokowani są legalni użytkownicy; zbyt łagodne i wyrafinowane boty się prześlizgują.
Same sygnały to łatwa część. Niemal każdy framework automatyzujący zdradza wszystkie 12. Prawdziwa inżynieria polega na decydowaniu, które kombinacje mają znaczenie, które są fałszywymi trafieniami i jak aktualizować model w miarę adaptacji frameworków automatyzujących.