Ton WAF tourne chez l'adversaire

Cenros avatar
Cenros

Le check de 4 lignes qui protege ton WAF a six chiffres

Ton WAF enterprise a coute six chiffres. Et sa premiere question pour savoir si tu es un robot, c’est un check de 4 lignes de JavaScript.

Le principe : quand un bot falsifie son canvas, sa timezone ou son GPU, il remplace des fonctions natives du navigateur par ses propres versions. Le WAF appelle toString() sur ces fonctions. Une vraie native retourne "[native code]". Une fonction modifiee retourne son code source. Detection immediate.

Tous les WAF enterprise l’utilisent. Voila comment ca tombe.

L’attaquant ne cherche pas a empecher le check. Il remplace toString lui-meme. Le WAF appelle toString() sur une fonction modifiee, toString repond "[native code]", et le WAF valide. Le check fonctionne parfaitement.. il valide juste un mensonge.

Les WAF ont escalade :

  • Appeler toString(toString) pour verifier si toString lui-meme est modifie.. Le bot a contre en s’auto-validant.
  • Inspecter la chaine de prototypes pour detecter des getters injectes.. Quelques semaines de repit, puis les bots ont nettoye les traces.
  • Provoquer des erreurs et analyser les stack traces.. Les bots ont intercepte la creation d’erreurs pour sanitizer les stack traces.

Meme cycle a chaque fois : le WAF trouve un angle, ca tient quelques semaines, le bot s’adapte. Un check qui resistait 6 mois en 2023 tient quelques semaines aujourd’hui.

Et c’est pas un probleme de qualite. C’est l’architecture. Le bot tourne dans SON navigateur, sur SA machine. Tout ce que ton JavaScript observe, le bot peut le falsifier. Tu empiles les verifications, tu lui demandes juste de mentir un cran plus profond.

Ta main qui tremble, c’est ta preuve d’humanite

Quand tu poses ta souris et que tu lis un paragraphe, ton curseur n’est jamais parfaitement immobile. Les micro-tremblements musculaires involontaires de ta main produisent des deplacements de +/-1-3 pixels, toutes les 50 a 200 millisecondes. C’est invisible a l’oeil. Le sensor JavaScript de ton WAF, lui, il compte.

0 mouvement de souris pendant plus de 2 secondes = flag “curseur gele”. Signal envoye au serveur. Parce qu’un curseur parfaitement statique, en 2026, c’est la signature d’un programme. Pas d’un humain.

En testant, on a contourne ce check en 20 minutes. Un micro-deplacement gaussien autour de la position courante, +/-1-3 pixels en X avec un sigma legerement plus large qu’en Y (parce qu’un droitier tremble plus horizontalement que verticalement), toutes les 50-200ms pendant les pauses.

Le WAF ne voit plus la difference.

Et c’est ca le probleme de la detection behavioral : chaque signal humain qu’un WAF apprend a mesurer, c’est aussi un signal qu’on apprend a simuler. La difference entre un trembleur reel et un trembleur simule, c’est un parametre de gaussienne. Rien d’autre.

Les fonts systeme - le signal qui resiste

Ton navigateur headless tourne sous Linux. Il a une trentaine de fonts. Chrome sur Windows en a plus de 200.

Ton WAF le sait.

Le check : le sensor JS cree un canvas, ecrit un texte en Segoe UI a 72px, et mesure la largeur avec measureText(). Si Segoe UI n’est pas installee, le navigateur fallback sur la font par defaut. Le WAF teste 4-5 fonts Windows (Segoe UI, Tahoma, Lucida Console, Calibri). Si toutes les largeurs sont identiques.. c’est un fallback. C’est un headless Linux.

Ce qui rend ce signal tres dur a contourner, c’est qu’il est physique. measureText() depend du moteur de rendu graphique de l’OS - FreeType sous Linux, ClearType sous Windows. Meme en installant les fonts Windows sur le Linux, les largeurs de glyphes different de quelques pixels parce que le hinting et l’anti-aliasing ne sont pas les memes.

L’alternative c’est d’override measureText() en JS pour retourner les bonnes valeurs. Sauf qu’il faut maintenir une table de largeurs pour chaque font x chaque taille x chaque string que le WAF pourrait tester. En pratique, c’est un cauchemar de maintenance.

Le bot peut mentir sur son User-Agent. Sur son canvas. Sur son GPU. Sur ses plugins. Sur son timezone. Mais il ne peut pas mentir sur ce que son OS sait dessiner.

La limite structurelle

La limite de toute detection JS statique : tu fais tourner ton code chez l’adversaire. Son terrain, ses regles.

Les checks toString filtrent le bruit. C’est un ralentisseur, pas un mur. Les checks behavioral se contournent avec les bons parametres statistiques. Les fonts systeme resistent plus longtemps parce que le signal est physique, pas logiciel.

La detection qui tient se joue ailleurs :

  • Cote serveur : le ratio HTML/assets dans tes access logs (un bot ne charge que le HTML).
  • Cote reseau : l’empreinte TCP/TLS geree par le kernel, pas le navigateur.
  • Cote economique : rendre le bot trop cher a operer.

Si ta ligne de defense principale repose sur du JavaScript, la seule chose que tu testes c’est si l’attaquant a lu le meme article de blog que toi.

En general, il l’a lu.