Comment fonctionne réellement la détection de Puppeteer : 12 signaux qui trahissent les bots
Les frameworks d'automatisation héritent de l'empreinte d'un vrai navigateur mais la modifient de dizaines de façons observables. Guide des 12 signaux JavaScript, réseau et comportementaux qui trahissent Puppeteer et Playwright.
Les frameworks d'automatisation contrôlent de vrais navigateurs, ce qui signifie qu'ils héritent de l'empreinte du navigateur. Un Chrome piloté par Puppeteer a le même user agent, la même version de Chrome et le même build Chromium que le Chrome d'un humain. En surface, ils sont identiques.
Mais les frameworks d'automatisation modifient le navigateur de dizaines de façons subtiles — la plupart observables depuis JavaScript. La détection n'a rien de magique. C'est une checklist.
1. Le flag navigator.webdriver
Le signal le plus simple et le plus célèbre. Quand Chrome tourne sous un framework d'automatisation, navigator.webdriver renvoie true. Cela fait partie de la spécification W3C WebDriver et Chromium l'implémente par défaut.
Tout projet d'automatisation sérieux patche cela en quelques secondes. Puppeteer-extra-stealth surcharge le getter. Les utilisateurs de Playwright injectent des scripts Object.defineProperty sur les nouveaux documents. Le flag est trivial à cacher.
Mais le fait qu'il puisse être trivialement caché est lui-même un signal. Un Chrome légitime n'a rien à cacher. Quand une page vérifie Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver') et que le descripteur diffère de celui d'un navigateur natif — l'automatisation est attrapée même après le patch du flag.
2. L'anomalie de l'objet chrome
Un Chrome légitime expose un objet global window.chrome à la structure substantielle — chrome.runtime, chrome.loadTimes, chrome.csi. Chrome headless et les anciennes configurations Puppeteer omettent entièrement cet objet ou en exposent une version amputée.
Les plugins stealth recréent l'objet, mais la recréation est imparfaite. chrome.runtime.onConnect peut être présent sans chrome.runtime.PlatformOs. Des signatures de fonction peuvent renvoyer des objets au lieu d'undefined. Chaque divergence est un signal positif.
3. Incohérences de l'API Permissions
Les vrais navigateurs renvoient des résultats cohérents quand on les interroge via l'API Permissions. navigator.permissions.query({name: 'notifications'}) devrait renvoyer 'default' si l'utilisateur n'a pas explicitement accordé ou refusé la permission.
Chrome headless renvoie 'denied' par défaut, parce qu'il n'y a pas d'UI pour afficher une invite de permission. Les frameworks d'automatisation patchent cela — mais souvent incorrectement. Un indice courant : interroger une permission inhabituelle comme 'clipboard-read' renvoie un résultat qui ne correspond pas à ce que fait le vrai Chrome dans la version courante.
4. Tableaux de plugins et de types MIME
navigator.plugins dans un vrai navigateur renvoie un PluginArray avec des entrées comme le lecteur PDF et le plugin PDF de Chromium. En mode headless, ce tableau est vide.
Les plugins stealth ajoutent de fausses entrées, mais celles-ci ont fréquemment de mauvaises propriétés — length manquant, champs de description erronés, ou objets plugin qui ne se comportent pas comme des instances de Plugin quand on les teste avec instanceof.
5. Divergences de langue et de locale
navigator.language et navigator.languages doivent correspondre à l'en-tête HTTP Accept-Language. Ils doivent aussi être cohérents avec le fuseau horaire rapporté par Intl.DateTimeFormat().resolvedOptions().timeZone.
Un bot qui prétend être à Kyiv (fuseau Europe/Kyiv) mais envoie Accept-Language: en-US,en;q=0.9 et rapporte navigator.language === 'en-US' est possible mais statistiquement inhabituel. Combiné à une IP d'un datacenter de Francfort, le tableau devient clair.
6. Chaînes de renderer WebGL
WebGL2RenderingContext.getParameter(WebGLDebugRendererInfo.UNMASKED_RENDERER_WEBGL) renvoie le fournisseur et le modèle du GPU. Sur une vraie machine, cela peut être :
ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0)
Chrome headless dans un conteneur renvoie souvent :
ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)
Toute chaîne GPU contenant SwiftShader ou Subzero est un signal de bot fort.
7. Anomalies d'écran et de viewport
Une vraie fenêtre de navigateur a des window.outerWidth et window.outerHeight qui tiennent compte du chrome du navigateur — barre d'adresse, onglets, barre de favoris. La différence entre dimensions externes et internes est typiquement de 80 à 140 pixels en vertical.
Les navigateurs headless ont souvent outerHeight === innerHeight parce qu'il n'y a pas de chrome à afficher. Puppeteer avec headless: false corrige cela, mais beaucoup de configurations d'automatisation tournent encore avec des dimensions égales.
8. Énumération des polices
Les utilisateurs réels ont des centaines de polices installées, variant selon l'OS, la langue et les applications installées. Un Windows 11 avec Office compte plus de 500 polices. Les conteneurs d'automatisation sous Alpine Linux en ont 30.
L'énumération des polices via document.fonts.check() ou le rendu sur canvas hors écran révèle cela immédiatement. Un navigateur qui se déclare Chrome sous Windows 11 mais n'expose que 30 polices est presque certainement de l'automatisation.
9. Entropie des mouvements de souris
Les utilisateurs réels bougent leur souris avec du jitter. Les courbes sont non linéaires. La vitesse varie. Entre deux clics, il y a typiquement des centaines d'événements mousemove.
Les frameworks d'automatisation synthétisent le mouvement — soit en lignes droites vers les coordonnées, soit avec des courbes programmées dépourvues des micro-tremblements d'une main humaine. Même les simulations à courbes de Bézier tendent à avoir des profils d'accélération étrangement lisses.
Les systèmes de détection de bots collectent les traces de mouvement et les scorent contre un modèle du mouvement humain. Les traces à la vélocité trop régulière, aux courbes trop propres ou aux micro-événements manquants sont signalées.
10. Signatures de timing
Les frameworks d'automatisation exécutent JavaScript avec des caractéristiques de timing différentes de celles des navigateurs pilotés par un utilisateur. Le timing performance.now() entre événements, en particulier entre pointerdown et pointerup, suit une distribution différente.
Les durées réelles de maintien d'un clic sont de 50 à 150 ms avec une forte variance. La durée de maintien par défaut de Puppeteer est une valeur fixe — souvent 30 ms ou 100 ms — avec presque aucune variance. Cent clics avec des durées de maintien identiques, c'est de l'automatisation caractérisée.
11. Divergence d'empreinte TLS
Celui-ci contourne entièrement JavaScript. Quand Puppeteer se connecte, il utilise la stack TLS de Chromium — la même qu'un vrai Chrome. Mais les scrapers basés sur undici ou axios qui se font passer pour Chrome via l'usurpation du user agent utilisent la stack TLS de Node.js, dont la signature Client Hello (hachage JA4) est nettement différente.
Une requête revendiquant User-Agent: Chrome/124.0.6367.60 mais présentant une empreinte TLS de Node.js est la confirmation instantanée d'un client non-navigateur.
12. Fuites du protocole CDP
Puppeteer et Playwright communiquent avec Chrome via le Chrome DevTools Protocol. Dans certaines configurations, ce protocole laisse des artefacts détectables — des propriétés supplémentaires sur l'objet Runtime, des formats d'Error.stack altérés, ou des sorties console.debug spécifiques qui n'apparaissent que lorsqu'un client CDP est attaché.
La commande Runtime.enable en particulier change la manière dont les stack traces sont rendues. Les bots qui ne déclenchent jamais d'erreur évitent ce signal, mais tout flux d'automatisation qui rencontre une exception (et c'est le cas de la plupart) laisse des empreintes.
Empiler ces signaux
Aucun signal isolé ne prouve l'automatisation. Un utilisateur réel avec un pilote GPU inhabituel peut renvoyer une chaîne WebGL étrange. Un utilisateur soucieux de sa vie privée peut avoir modifié navigator.plugins.
La force de la détection moderne vient de la combinaison des signaux — 12 positifs faibles sont plus forts qu'un seul positif fort, parce qu'un utilisateur réel déclenche rarement plus de 2 ou 3 anomalies simultanément.
Les systèmes de détection assignent un poids à chaque signal et calculent un score. Au-dessus du seuil, le visiteur est traité comme de l'automatisation. Le seuil exact et les poids sont les parties les plus difficiles à régler — trop stricts, et des utilisateurs légitimes sont bloqués ; trop laxistes, et les bots sophistiqués passent.
Les signaux eux-mêmes sont la partie facile. Presque tous les frameworks d'automatisation laissent fuir les 12. Le vrai travail d'ingénierie consiste à décider quelles combinaisons comptent, lesquelles sont des faux positifs, et comment mettre à jour le modèle à mesure que les frameworks d'automatisation s'adaptent.