Hoe Puppeteer-detectie echt werkt: 12 signalen die bots verraden
Automatiseringsframeworks erven de fingerprint van een echte browser, maar wijzigen die op tientallen waarneembare manieren. Een veldgids voor de 12 JavaScript-, netwerk- en gedragssignalen waarmee Puppeteer en Playwright worden betrapt.
Automatiseringsframeworks besturen echte browsers, wat betekent dat ze de fingerprint van de browser erven. Een door Puppeteer aangestuurde Chrome heeft dezelfde user agent, dezelfde Chrome-versie en dezelfde Chromium-build als de Chrome van een mens. Oppervlakkig gezien zien ze er identiek uit.
Maar automatiseringsframeworks wijzigen de browser op tientallen subtiele manieren — de meeste ervan waarneembaar vanuit JavaScript. Detectie is geen magie. Het is een checklist.
1. De navigator.webdriver-vlag
Het eenvoudigste en bekendste signaal. Wanneer Chrome onder een automatiseringsframework draait, geeft navigator.webdriver true terug. Het maakt deel uit van de W3C WebDriver-specificatie en Chromium implementeert het standaard.
Elk serieus automatiseringsproject repareert dit binnen enkele seconden. Puppeteer-extra-stealth overschrijft de getter. Playwright-gebruikers injecteren Object.defineProperty-scripts op nieuwe documenten. De vlag is triviaal te verbergen.
Maar het feit dat hij triviaal te verbergen is, is zelf een signaal. Een legitieme Chrome hoeft niets te verbergen. Wanneer een pagina Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver') controleert en de descriptor er anders uitziet dan die van een native browser — dan is de automatisering betrapt, zelfs nadat de vlag is gerepareerd.
2. De afwijking van het chrome-object
Legitieme Chrome stelt een globaal window.chrome-object beschikbaar met een aanzienlijke structuur — chrome.runtime, chrome.loadTimes, chrome.csi. Headless Chrome en oudere Puppeteer-opstellingen laten dit object ofwel volledig weg, ofwel stellen ze een uitgeklede versie beschikbaar.
Stealth-plug-ins bouwen het object opnieuw op, maar de reconstructie is onvolmaakt. chrome.runtime.onConnect kan aanwezig zijn zonder chrome.runtime.PlatformOs. Functiesignaturen kunnen objecten teruggeven in plaats van undefined. Elke mismatch is een positief signaal.
3. Inconsistenties in de Permissions API
Echte browsers geven coherente resultaten terug wanneer ze via de Permissions API worden bevraagd. navigator.permissions.query({name: 'notifications'}) zou 'default' moeten teruggeven als de gebruiker geen toestemming expliciet heeft verleend of geweigerd.
Headless Chrome geeft standaard 'denied' terug, omdat er geen UI is om een toestemmingsverzoek te tonen. Automatiseringsframeworks repareren dit — maar vaak onjuist. Een veelvoorkomende verklikker: het bevragen van een ongebruikelijke permissie zoals 'clipboard-read' geeft een resultaat dat niet overeenkomt met wat echte Chrome in de huidige versie doet.
4. Arrays met plug-ins en MIME-types
navigator.plugins geeft in een echte browser een PluginArray terug met vermeldingen zoals de PDF-viewer en de Chromium PDF-plug-in. In headless-modus is deze array leeg.
Stealth-plug-ins voegen nep-vermeldingen toe, maar de vermeldingen hebben vaak verkeerde eigenschappen — een ontbrekende length, verkeerde description-velden, of plug-inobjecten die zich niet als Plugin-instanties gedragen wanneer ze met instanceof worden getest.
5. Taal- en localemismatches
navigator.language en navigator.languages zouden moeten overeenkomen met de Accept-Language HTTP-header. Ze zouden ook consistent moeten zijn met de tijdzone die door Intl.DateTimeFormat().resolvedOptions().timeZone wordt gerapporteerd.
Een bot die beweert in Kyiv te zitten (tijdzone Europe/Kyiv) maar Accept-Language: en-US,en;q=0.9 verstuurt en navigator.language === 'en-US' rapporteert, is mogelijk maar statistisch ongebruikelijk. In combinatie met een IP-adres van een datacenter in Frankfurt wordt het beeld duidelijk.
6. WebGL-rendererstrings
WebGL2RenderingContext.getParameter(WebGLDebugRendererInfo.UNMASKED_RENDERER_WEBGL) geeft de GPU-leverancier en het model terug. Op een echte machine kan dit zijn:
ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0)
Headless Chrome die in een container draait, geeft vaak terug:
ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)
Elke GPU-string die SwiftShader of Subzero bevat, is een sterk botsignaal.
7. Afwijkingen in scherm en viewport
Een echt browservenster heeft window.outerWidth en window.outerHeight die rekening houden met de browserchrome — adresbalk, tabbladen, bladwijzerbalk. Het verschil tussen de buiten- en binnenafmetingen is doorgaans 80–140 pixels verticaal.
Headless browsers hebben vaak outerHeight === innerHeight omdat er geen chrome is om weer te geven. Puppeteer met headless: false lost dit op, maar veel automatiseringsopstellingen draaien nog steeds met gelijke afmetingen.
8. Font-enumeratie
Echte gebruikers hebben honderden geïnstalleerde fonts, variërend per OS, taal en geïnstalleerde applicaties. Windows 11 met Office geïnstalleerd heeft 500+ fonts. Automatiseringscontainers die Alpine Linux draaien, hebben er 30.
Font-enumeratie via document.fonts.check() of offscreen canvas-rendering onthult dit onmiddellijk. Een browser die zichzelf als Windows 11 Chrome rapporteert maar slechts 30 fonts blootlegt, is vrijwel zeker automatisering.
9. Entropie van muisbewegingen
Echte gebruikers bewegen de muis met jitter. Curves zijn niet-lineair. De snelheid varieert. Tussen twee klikken zitten doorgaans honderden mousemove-events.
Automatiseringsframeworks synthetiseren beweging — ofwel met rechte lijnen naar coördinaten, ofwel met geprogrammeerde curves die de microtremors van een menselijke hand missen. Zelfs simulaties met bezierkrommen hebben doorgaans verdacht vloeiende versnellingsprofielen.
Botdetectiesystemen verzamelen bewegingssporen en scoren ze tegen een model van menselijke beweging. Sporen met een te consistente snelheid, te zuivere curves of ontbrekende micro-events worden gemarkeerd.
10. Timing-signaturen
Automatiseringsframeworks voeren JavaScript uit met andere timingkenmerken dan door gebruikers aangestuurde browsers. performance.now()-timing tussen events, vooral tussen pointerdown en pointerup, volgt een andere verdeling.
Echte klik-vasthoudtijden zijn 50–150 ms met een hoge variantie. Puppeteers standaard klik-vasthoudtijd is een vaste waarde — vaak 30 ms of 100 ms — met vrijwel geen variantie. Honderd klikken met identieke vasthoudtijden is definitieve automatisering.
11. TLS-fingerprintmismatch
Deze omzeilt JavaScript volledig. Wanneer Puppeteer verbinding maakt, gebruikt het de TLS-stack van Chromium — net als een echte Chrome. Maar op undici of axios gebaseerde scrapers die via user-agent-spoofing doen alsof ze Chrome zijn, gebruiken de TLS-stack van Node.js, die een duidelijk andere Client Hello-signatuur heeft (JA4-hash).
Een verzoek dat User-Agent: Chrome/124.0.6367.60 beweert maar een TLS-fingerprint van Node.js presenteert, is een directe bevestiging van een niet-browserclient.
12. CDP-protocollekkage
Puppeteer en Playwright communiceren met Chrome via het Chrome DevTools Protocol. In sommige configuraties laat dit protocol detecteerbare artefacten achter — extra eigenschappen op het Runtime-object, gewijzigde Error.stack-formats, of specifieke console.debug-uitvoer die alleen verschijnt wanneer een CDP-client is aangekoppeld.
Met name het commando Runtime.enable verandert de manier waarop stacktraces worden weergegeven. Bots die nooit fouten activeren, ontwijken dit signaal, maar elke automatiseringsflow die op een uitzondering stuit (en de meeste doen dat) laat fingerprints achter.
Deze signalen in lagen combineren
Geen enkel signaal is op zichzelf bewijs van automatisering. Een echte gebruiker met een ongebruikelijke GPU-driver kan een vreemde WebGL-string teruggeven. Een privacygerichte gebruiker kan navigator.plugins hebben aangepast.
De kracht van moderne detectie komt uit het combineren van signalen — 12 zwakke positieven zijn sterker dan één sterke positieve, omdat een echte gebruiker zelden meer dan 2–3 afwijkingen tegelijk activeert.
Detectiesystemen kennen aan elk signaal een gewicht toe en berekenen een score. Boven de drempel wordt de bezoeker als automatisering behandeld. De exacte drempel en gewichten zijn de moeilijkste onderdelen om goed te krijgen — te streng en legitieme gebruikers worden geblokkeerd; te soepel en geavanceerde bots glippen erdoor.
De signalen zelf zijn het eenvoudige deel. Vrijwel elk automatiseringsframework lekt alle 12. Het echte engineeringwerk is bepalen welke combinaties ertoe doen, welke fout-positieven zijn, en hoe je het model bijwerkt naarmate automatiseringsframeworks zich aanpassen.