De nos jours, de nombreux incidents de sécurité web impliquent l’automatisation. Les attaques de Web scraping, de réutilisation de mots de passe et de fraude au clic sont perpétrées par des adversaires qui tentent d'imiter de vrais utilisateurs et tenteront ainsi de donner l'impression qu'ils proviennent d'un navigateur. En tant que propriétaire de site Web, vous souhaitez vous assurer de servir les humains, et en tant que fournisseur de services Web, vous souhaitez que l'accès programmatique à votre contenu passe par votre API au lieu d'être récupéré via votre interface Web plus lourde et moins stable.
En supposant que vous disposez de vérifications de base pour les visiteurs de type cURL, la prochaine étape raisonnable consiste à vous assurer que les visiteurs utilisent de vrais navigateurs pilotés par l'interface utilisateur, et non des navigateurs sans tête comme PhantomJS et SlimerJS .
Dans cet article, nous allons démontrer quelques techniques permettant d’identifier les visites effectuées par PhantomJS. Nous avons décidé de nous concentrer sur PhantomJS car il s’agit de l’environnement de navigateur headless le plus populaire, mais de nombreux concepts que nous aborderons sont applicables à SlimerJS et à d’autres outils.
NOTE: Les techniques présentées dans cet article sont applicables à PhantomJS 1.x et 2.x, sauf mention explicite. Tout d’abord : est-il possible de détecter PhantomJS sans même y répondre ?
Comme vous le savez peut-être, PhantomJS est construit sur le framework Qt . La manière dont Qt implémente la pile HTTP le distingue des autres navigateurs modernes.
Commençons par examiner Chrome, qui envoie les en-têtes suivants :
Dans PhantomJS, cependant, la même requête HTTP ressemble à ceci :
Vous remarquerez que les en-têtes de PhantomJS sont distincts de ceux de Chrome (et, en fin de compte, de tous les autres navigateurs modernes) de plusieurs manières subtiles :
En vérifiant ces aberrations d'en-tête HTTP sur le serveur, il devrait être possible d'identifier un navigateur PhantomJS.
Mais est-il prudent de croire à ces valeurs ? Si un adversaire utilise un proxy pour réécrire les en-têtes devant le navigateur sans tête, il pourrait modifier ces en-têtes pour qu'ils ressemblent à un navigateur moderne normal.
Il semble que s’attaquer à ce problème uniquement au niveau du serveur ne soit pas une solution miracle. Voyons maintenant ce qui peut être fait sur le client, en utilisant l’environnement JavaScript de PhantomJS.
Nous ne pouvons peut-être pas faire confiance à la valeur de l’agent utilisateur telle que fournie via HTTP, mais qu’en est-il du côté client ?
Malheureusement, il est tout aussi simple de modifier l'en-tête de l'agent utilisateur et les valeurs de navigator.userAgent dans PhantomJS, donc cela pourrait ne pas être suffisant.
navigator.plugins contient un tableau de plugins présents dans le navigateur. Les valeurs de plug-in typiques incluent Flash, ActiveX, la prise en charge des applets Java et le « Default Browser Helper », qui est un plug-in qui indique si ce navigateur est le navigateur par défaut dans OS X. Dans nos recherches, la plupart des nouvelles installations de navigateurs courants incluent au moins un plug-in par défaut, même sur mobile.
Ceci est différent de PhantomJS, qui n'implémente aucun plugin et ne fournit pas de moyen d'en ajouter un (en utilisant l'API PhantomJS ).
La vérification suivante pourrait alors être utile :
D'un autre côté, il est assez simple d'usurper ce tableau de plugins en modifiant l'environnement JavaScript PhantomJS avant le chargement de la page .
Il n’est pas non plus difficile d’imaginer une version personnalisée de PhantomJS avec de vrais plugins implémentés. C'est plus facile qu'il n'y paraît car le framework Qt sur lequel PhantomJS est construit fournit une API native pour l'implémentation de plugins.
Un autre point intéressant est la façon dont PhantomJS supprime les boîtes de dialogue JavaScript :
Après avoir effectué plusieurs mesures, il apparaît que si la boîte de dialogue d'alerte est supprimée dans les 15 millisecondes, le navigateur n'est probablement pas contrôlé par un humain. Mais utiliser cette approche revient à déranger les vrais utilisateurs avec une alerte qu’ils devront fermer manuellement.
PhantomJS 1.x expose deux propriétés sur l'objet global :
Cependant, ces propriétés font partie d’une fonctionnalité expérimentale et peuvent changer à l’avenir.
PhantomJS 1.x et 2.x utilisent actuellement des moteurs WebKit obsolètes, ce qui signifie qu'il existe des fonctionnalités de navigateur qui existent dans les navigateurs plus récents et qui n'existent pas dans PhantomJS. Cela s’étend au moteur JavaScript — certaines propriétés et méthodes natives étant différentes ou absentes dans PhantomJS.
L'une de ces méthodes est Function.prototype.bind, qui manque dans PhantomJS 1.x et les versions antérieures. L'exemple suivant vérifie si bind est présent et qu'il n'a pas été usurpé dans l'environnement d'exécution.
Ce code est un peu trop compliqué à expliquer en détail ici, mais vous pouvez en savoir plus dans notre présentation .
Les erreurs générées par le code JavaScript évalué par PhantomJS via la commande d'évaluation contiennent une trace de pile identifiable de manière unique, à partir de laquelle nous pouvons identifier le navigateur sans tête.
Par exemple, supposons que PhantomJS appelle evaluation sur le code suivant :
Notez que cet exemple utilise une fonction indexOfString() personnalisée, laissée en exercice pour le lecteur, puisque le String.prototype.indexOf natif peut être usurpé par PhantomJS pour toujours renvoyer un résultat négatif.
Maintenant, comment faire pour qu'un script PhantomJS évalue ce code ? Une technique consiste à remplacer certaines fonctions API DOM fréquemment utilisées qui sont susceptibles d’être appelées. Par exemple, le code ci-dessous remplace document.querySelectorAll pour inspecter la trace de la pile du navigateur :
Dans cet article, nous avons examiné 7 techniques différentes pour identifier PhantomJS, à la fois sur le serveur et en exécutant du code dans l’environnement JavaScript client de PhantomJS. En combinant les résultats de détection avec un mécanisme de rétroaction puissant (par exemple, en rendant une page dynamique inerte ou en invalidant le cookie de session actuel), vous pouvez introduire un obstacle solide pour les visiteurs de PhantomJS. Gardez toutefois à l’esprit que ces techniques ne sont pas infaillibles et qu’un adversaire sophistiqué y parviendra éventuellement.
Pour en savoir plus, nous vous recommandons de regarder cet enregistrement de notre présentation d'AppSec USA 2014 ( diapositives ). Nous avons également rassemblé un référentiel GitHub d’exemples d’implémentations – et de contournements possibles – des techniques présentées ici.
Merci de votre lecture et bonne chasse.
Sergey Shekyan – @sshekyan
Ben Vinegar – @bentlegen
Bei Zhang – @ikarienator