Retour au Centre de connaissances
Empreinte

Plugins de correctifs navigateur : ce qu’ils changent

Lisez la liste publique des contournements d’un plugin Puppeteer répandu, puis voyez quelles trois surfaces un script de page ne peut pas atteindre.

BotBrowser Team

Documentation

Vous voulez la documentation structurée pour Empreinte ?

Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.

Un plugin de ce genre est un ensemble de correctifs en JavaScript de page, et la forme de cette livraison décide à la fois de ce qu’il peut changer et de ce qu’il ne peut pas changer. Le paquet puppeteer-extra-plugin-stealth fournit un répertoire de contournements, chacun étant un petit script evaluateOnNewDocument qui réécrit une propriété de navigator, un membre de l’espace de noms chrome ou un accesseur WebGL avant que le code de la page ne s’exécute. C’est un mécanisme réel et inspectable, et il a une frontière : un correctif qui s’exécute dans la page peut être lu depuis la page, et trois surfaces ne passent jamais par le JavaScript de page. Lire le code source public de ce paquet suffit à lister ce qu’il corrige, à nommer les trois surfaces qu’il ne peut pas atteindre et à construire un banc d’essai qui lit valeur, descripteur et toString côte à côte, pour qu’un lecteur puisse dire si un correctif s’est réellement appliqué. Les variantes Puppeteer et Playwright sont des projets tiers distincts avec leurs propres cycles de publication, et aucune des deux n’est recommandée. Un taux de réussite n’est pas quelque chose qu’un changement local peut promettre.

BotBrowser fournit une autre voie de livraison pour les mêmes valeurs de document : les réglages de profil sont appliqués à l’intérieur du cœur du navigateur, donc une valeur et le chemin de code qui la produit viennent d’une même couche, comme le documente la référence de cohérence d’automatisation. Cette couche reste d’un seul côté de la frontière décrite plus bas, et elle ne garantit pas un verdict tiers ; les surfaces réseau et processus restent hors de sa portée.

TL;DR

  • Les contournements du plugin sont des scripts de page injectés avant le code du document, pas de la configuration de navigateur. Chacun réécrit une valeur visible en JavaScript, et une page peut relire le résultat.
  • Le répertoire public des contournements contient 19 entrées : chrome.app, chrome.csi, chrome.loadTimes, chrome.runtime, defaultArgs, iframe.contentWindow, media.codecs, navigator.hardwareConcurrency, navigator.languages, navigator.permissions, navigator.plugins, navigator.vendor, navigator.webdriver, sourceurl, user-agent-override, webgl.vendor, window.outerdimensions, plus les dossiers partagés _template et _utils.
  • navigator.webdriver est traité deux fois : la couche d’arguments ajoute --disable-blink-features=AutomationControlled, et le script de page supprime la propriété du prototype sur les anciennes compilations Chromium où elle est encore présente.
  • Une valeur corrigée et une valeur native sont indiscernables avec console.log. Lire Object.getOwnPropertyDescriptor et Function.prototype.toString est ce qui sépare une propriété réécrite d’une propriété native.
  • Trois surfaces n’atteignent jamais le JavaScript de page : les empreintes de la couche réseau, la poignée de main TLS et les réglages HTTP/2 ; le processus du navigateur et sa ligne de commande ; et tout ce qu’un serveur décide depuis la connexion plutôt que depuis le document.

Un schéma en couches montre des correctifs JavaScript de page atteignant trois surfaces du document, tandis que trois autres surfaces se situent sous la frontière JavaScript : la couche réseau et TLS, le processus du navigateur et sa ligne de commande, et la décision côté serveur.

Sommaire

Ce que contient réellement la liste publique

Le code du plugin est un répertoire nommé evasions. Le lire suffit à savoir ce que le plugin touche, parce que chaque sous-répertoire est un contournement nommé avec son propre index.js.

ContournementSurface viséeCe que le script change
navigator.webdriverDrapeau d’automatisationSupprime la propriété du prototype sur les anciennes compilations ; ajoute un argument de lancement sur les récentes
navigator.vendorIdentité du navigateurRéécrit la chaîne de fournisseur
navigator.pluginsÉnumération des pluginsRemplace le contenu du tableau
navigator.languagesListe de languesRéécrit le tableau rapporté
navigator.permissionsInterrogation des permissionsEnveloppe le résultat de la requête
navigator.hardwareConcurrencyNombre de cœursRéécrit l’entier rapporté
webgl.vendorParamètre WebGLEnveloppe getParameter pour les chaînes de fournisseur et de moteur
chrome.app, chrome.csi, chrome.loadTimes, chrome.runtimeEspace de noms chromeAjoute ou réécrit des membres lisibles par la page
media.codecsPrise en charge des codecsRéécrit la réponse sur les codecs
iframe.contentWindowIdentité entre cadresAligne les valeurs rapportées par la fenêtre d’iframe
window.outerdimensionsGéométrie de fenêtreRapporte la taille extérieure d’une fenêtre normale
user-agent-overrideEn-tête et valeur d’agentDéfinit l’en-tête et la chaîne visible par la page
sourceurlURL source du scriptRetire le marqueur sourceURL que Puppeteer ajoute à ses scripts injectés
defaultArgsArguments de lancementÉcarte les valeurs par défaut de Puppeteer qui rendent le navigateur plus facile à identifier

Deux faits structurels comptent plus que les lignes individuelles. D’abord, chaque ligne réécrit une valeur qu’une page peut lire, ce qui signifie que la réécriture est aussi une valeur qu’une page peut inspecter. Ensuite, la liste est finie et publique, donc un auteur de détecteur peut lire le même répertoire et sonder exactement ces surfaces.

Le mécanisme de livraison est la contrainte déterminante

evaluateOnNewDocument exécute une fonction avant tout script de page. C’est ce moment qui rend le correctif invisible à une lecture naïve : quand le code de la page s’exécute, la propriété porte déjà la valeur réécrite. Mais le correctif ne change pas le navigateur ; il change ce que le document voit après que le navigateur a produit une valeur.

Cette distinction explique toute la classe de défaillance. Un serveur qui décide depuis la poignée de main TLS n’exécute jamais le script de page, donc aucun evaluateOnNewDocument ne l’atteint. Un client CDP qui inspecte la ligne de commande de la cible lit les arguments du processus, pas le document, donc une réécriture au niveau de la page y est également sans effet.

Les deux familles de plugins ne partagent pas leur code

puppeteer-extra et playwright-extra sont deux paquets distincts du même dépôt, chacun avec sa propre interface de plugin. Un plugin écrit pour l’un ne se charge pas dans l’autre sans adaptateur, et les deux écosystèmes mettent à jour leurs points d’injection selon des calendriers différents. Cela compte pour une comparaison : une liste de contournements lue dans le paquet Puppeteer décrit ce que ce paquet corrige dans le chemin de lancement Puppeteer, et la variante Playwright est un artefact différent avec sa propre liste.

La conséquence pratique est qu’une affirmation sur un plugin a besoin d’une famille et d’une version avant de signifier quoi que ce soit. Deux équipes peuvent dire que leur navigateur exécute un plugin de ce nom et exécuter du code différent avec une couverture différente, et la différence n’est pas visible de l’extérieur.

Ce que fait réellement le contournement de webdriver

Le contournement navigator.webdriver a deux moitiés, et lire les deux est instructif. Le crochet beforeLaunch modifie les arguments de lancement : si --disable-blink-features= est déjà présent, il ajoute AutomationControlled, sinon il ajoute le drapeau entier. Le crochet onPageCreated exécute ensuite un script de page qui vérifie la valeur courante et supprime Object.getPrototypeOf(navigator).webdriver uniquement sur les compilations où la propriété est encore présente et vraie.

Le plugin est donc honnête sur sa propre frontière : sur une compilation Chromium où le drapeau produit déjà false ou undefined, le script de page ne fait rien du tout. C’est un détail de conception utile pour quiconque lit une revendication de plugin, parce qu’il montre que même les auteurs du plugin traitent l’argument de lancement comme la correction principale et le script de page comme un correctif de compatibilité.

Ce qu’un correctif JavaScript de page ne peut pas atteindre

Trois groupes de surfaces se situent sous le document ou à côté de lui, et aucun script evaluateOnNewDocument ne peut les changer.

La couche réseau et TLS

Le ClientHello TLS, l’ordre des suites de chiffrement, les extensions, la liste ALPN, la trame SETTINGS HTTP/2 et l’ordre des en-têtes sont produits par la pile réseau du navigateur avant que tout document existe. Un serveur qui empreinte la connexion voit une forme à laquelle le script de page n’a jamais eu accès. Ajouter ou supprimer une propriété sur navigator ne change pas un seul octet de cette poignée de main.

Le même raisonnement couvre le comportement DNS et la réutilisation de connexion. Un script de correction au niveau de la page ne peut pas changer quel résolveur a répondu, si une connexion a été réutilisée ou combien de temps a pris la poignée de main TCP, parce que ces événements se terminent avant la première ligne de code de page.

Le processus du navigateur et la ligne de commande

L’arbre des processus, le chemin de l’exécutable, les arguments dans /proc/<pid>/cmdline sous Linux et la relation parent-enfant entre un lanceur et son navigateur sont observables depuis l’extérieur du document. Un script de page peut réécrire ce que le document rapporte de lui-même, mais il ne peut pas réécrire le vecteur d’arguments qui a démarré le processus.

C’est aussi pourquoi le plugin modifie args dans beforeLaunch plutôt que d’essayer de cacher les drapeaux à la page seule. Les drapeaux sont visibles pour quiconque peut lire le processus, et le propre commentaire du plugin sur la barre d’information de Chrome reconnaît que l’argument a une conséquence visible.

La décision côté serveur

Le dernier groupe n’est pas une empreinte du tout ; c’est le modèle qui consomme les lectures. Un service de contrôle peut combiner la forme du réseau, les en-têtes de requête, le minutage, l’historique du compte et les valeurs rapportées par la page dans un score, et aucune de ces entrées ne passe par le script de page comme une valeur qu’il peut modifier. Même un correctif parfait dans le document laisse le score au fournisseur. Les lectures qu’un produit donné échantillonne sont recensées dans ce qu’un détecteur lit réellement, et deux produits n’échantillonnent pas forcément le même ensemble.

C’est la frontière la plus profonde, et c’est celle qui fait de l’expression « navigateur non détecté » une affirmation sur un modèle plutôt qu’une propriété d’un navigateur.

Un banc d’essai qui lit valeur, descripteur et toString

Un banc d’essai qui ne lit que navigator.webdriver ne peut pas distinguer une valeur corrigée d’une valeur native, parce que les deux renvoient un booléen. Lire trois choses côte à côte les sépare.

Les trois lectures

La première est la valeur elle-même : navigator.webdriver, ou navigator.vendor, ou un résultat de WebGLRenderingContext.getParameter. C’est ce qu’un détecteur naïf lit.

La deuxième est le descripteur de propriété : Object.getOwnPropertyDescriptor(Object.getPrototypeOf(navigator), 'webdriver'). Un accesseur natif a une fonction get dont le texte source nomme l’accesseur intégré. Une propriété supprimée renvoie undefined ; une propriété redéfinie renvoie un descripteur dont le get pointe vers une enveloppe, qui peut porter ou non les mêmes drapeaux que l’accesseur natif.

La troisième est Function.prototype.toString sur ce getter. Le texte source d’un accesseur natif porte sa propre signature et se termine par [native code] ; sur une compilation Chromium, le getter de Navigator.prototype.webdriver se lit function get webdriver() { [native code] }. Une enveloppe définie en JavaScript montre son propre corps. Le dossier _utils du plugin contient un assistant withUtils qui corrige toString exactement pour cette raison, ce qui est en soi une preuve que la lecture fonctionne. Ce qu’un toString blanchi laisse encore voir est détaillé séparément dans les mensonges de remplacement natif et une liste blanche de toString, qui énumère les contrôles qui survivent à un texte source réécrit.

LectureRéponse nativeRéponse corrigéeCe qu’elle ne peut pas prouver
Valeur lueUn booléen, une chaîne de fournisseur, une chaîne de moteurLa valeur réécriteLaquelle l’a produite
getOwnPropertyDescriptorUn accesseur natif avec get et setundefined, ou un accesseur redéfiniSi la page est par ailleurs cohérente
Function.prototype.toString[native code]Le source de l’enveloppeSi l’enveloppe est convaincante

Pourquoi le banc d’essai doit tourner sur une page maîtrisée

La lecture doit s’exécuter sur un document que l’opérateur contrôle, parce que le but est de comparer une configuration connue à un document connu, et non de sonder le site de quelqu’un d’autre. Une URL data: locale ou un fichier statique sur disque suffit : le script du banc lit les trois valeurs, les imprime en JSON et range le résultat à côté du relevé d’exécution.

La comparaison devient alors triviale. Si la valeur diffère de ce que la compilation du navigateur rapporterait nativement, le descripteur indique si la propriété a été supprimée ou remplacée, et toString indique si l’enveloppe s’annonce. Un correctif qui laisse une chaîne [native code] là où vit une enveloppe est un correctif qui a aussi corrigé toString, ce qui est exactement le cas que l’assistant _utils du plugin existe pour traiter.

Pourquoi le banc d’essai a besoin d’une référence propre

La comparaison ne fonctionne que si le banc a quelque chose à quoi se comparer. Lisez les mêmes trois valeurs sur la même compilation avec le plugin désactivé, et rangez cette référence à côté de l’exécution corrigée. Sans elle, une chaîne [native code] ne prouve rien, parce que le lecteur n’a aucun relevé de ce à quoi ressemblait l’accesseur non corrigé sur cette compilation précise.

Une seconde référence vaut la peine d’être gardée : la même lecture depuis un lancement simple et non corrigé d’un Chromium standard de la même version majeure. Cette paire montre quelles différences viennent du plugin et lesquelles viennent de la compilation elle-même, parce qu’un changement de version de Chromium peut modifier les drapeaux de descripteur d’un accesseur sans qu’aucun plugin n’intervienne.

Lire un correctif qui prétend être natif

Le cas intéressant est une enveloppe dont toString a aussi été corrigé pour renvoyer [native code]. Cela ne rend pas la lecture inutile ; cela change ce que la lecture prouve. Un descripteur dont le get n’est pas le getter du moteur diffère encore de la référence non corrigée relevée sur la même compilation, et une enveloppe qui prétend être [native code] alors que le texte source lu dans un autre realm diffère de la référence non corrigée est en contradiction avec elle-même. La comparaison d’identité entre realms ne convient pas ici : le getter d’une iframe fraîche est un objet fonction différent même sur un navigateur sans aucun correctif, donc cette comparaison signale une différence dans les deux cas.

Le banc d’essai doit donc enregistrer aussi les drapeaux du descripteur : configurable, enumerable, writable et la présence ou l’absence de set. Un navigator.webdriver natif est un accesseur non inscriptible avec un ensemble de drapeaux stable, mais les drapeaux méritent d’être consignés sans être le critère : une évasion qui supprime la propriété ne laisse aucun descripteur, tandis que l’assistant replaceProperty du plugin étend le descripteur existant et ne remplace que le get, donc un getter substitué conserve les drapeaux natifs. Ce relevé ne demande pas à la page d’être honnête sur toString.

Consigner ce que le banc d’essai a vu

Rangez les trois lectures plus l’identifiant de compilation du navigateur, la version du script injecté et l’URL du document dans un seul objet JSON par exécution. Deux exécutions avec la même configuration doivent produire les mêmes lectures ; une différence entre elles signifie que l’injection ne s’est pas appliquée lors d’une exécution, ce qui est un défaut de configuration plutôt qu’un résultat de détecteur.

Ce que le banc d’essai ne peut pas trancher

Le banc décrit le document. Il ne décrit ni la connexion, ni le processus, ni le modèle. Une lecture parfaite d’un navigator.webdriver corrigé sur une page maîtrisée ne dit rien de la façon dont un fournisseur notera la session, et cet écart est la raison pour laquelle un relevé par exécution doit nommer la surface qu’il a mesurée.

Gardez cette frontière dans le relevé lui-même. Un champ étiqueté surface: document est honnête ; la même lecture étiquetée verdict: pass ne l’est pas, parce que le banc n’a jamais exécuté le modèle du fournisseur.

FAQ

Un correctif au niveau de la page change-t-il l’empreinte réseau du navigateur ?

Non. Ses contournements sont des scripts de page injectés avant le code du document, plus un changement d’argument de lancement. Les octets du ClientHello TLS, la trame SETTINGS HTTP/2 et l’ordre des en-têtes sont produits par la pile réseau avant que tout document existe, donc un script de page ne peut pas les atteindre.

Un navigator.webdriver corrigé suffit-il à passer un détecteur ?

Non. Un détecteur peut interroger la valeur, le descripteur de propriété, toString, les arguments de lancement ou la forme du réseau, et c’est le modèle du fournisseur qui choisit lesquelles il combine. L’évasion touche une seule propriété du prototype par deux chemins de code, tandis que les drapeaux du descripteur, le texte source lu dans un autre realm et la forme de la connexion restent ce que le lancement a produit.

Comment distinguer une propriété corrigée d’une propriété native ?

Lisez trois choses sur une page maîtrisée : la valeur, Object.getOwnPropertyDescriptor sur le prototype, et Function.prototype.toString sur le get du descripteur. Consignez les drapeaux du descripteur pour l’exécution, puis lisez le texte source dans le realm de l’iframe avec le Function.prototype.toString de ce realm et comparez-le à la référence non corrigée relevée sur la même compilation. Une enveloppe dont toString a été corrigé diffère encore de la référence une fois le texte source consigné comparé.

Conclusion pratique

Partez de la liste des contournements plutôt que du mot qui les désigne. Le répertoire public nomme chaque surface que le plugin touche, et chacune est une valeur qu’une page peut relire. Acceptez ensuite les trois surfaces qui sont sous le document : la couche réseau et TLS, le processus du navigateur et sa ligne de commande, et le modèle côté serveur qui consomme toutes les lectures.

L’action suivante tient en une exécution de banc d’essai sur une page maîtrisée. Lisez la valeur, le descripteur et toString pour navigator.webdriver, imprimez-les en JSON à côté de la compilation du navigateur et de la version du script injecté, et répétez l’exécution une fois. Si les deux exécutions concordent, le correctif s’applique de façon cohérente ; si elles diffèrent, c’est l’injection qu’il faut examiner, pas le détecteur. Matériel adjacent : ce qu’un détecteur lit réellement couvre la liste des surfaces du point de vue du détecteur, et le registre de détecteurs BotD montre quels contrôles exécute un détecteur open source.

Sources

#Plugin Navigateur#Puppeteer-Extra#Navigator.webdriver#Function toString#Descripteur

Cet article fait partie du guide : Qu'est-ce qu'une empreinte de navigateur ?

Exécutez la même vérification dans BotBrowser

Chargez un profil, gardez dans le navigateur les signaux que vous venez de lire, puis confirmez le résultat sur votre propre machine avant le déploiement.