Vous écrivez une regex, elle survole les tests unitaires, puis soudain une saisie utilisateur en production fait grimper le serveur à 100 % de CPU — toutes les requêtes se mettent en file et meurent. Vous redémarrez le service, les logs ne montrent aucune erreur. La semaine suivante, une autre saisie déclenche le même blocage, et l’équipe ops ne peut que rejeter la faute sur « des saisies utilisateur bizarres ».
C’est le backtracking catastrophique en regex. Ce n’est pas un bug dans votre code — c’est la structure de votre regex qui devient exponentiellement lente sur certaines entrées. Vous ne pouvez pas le reproduire en local parce que les tests utilisent des entrées « normales » ; ce qui le déclenche, ce sont des entrées « presque-correspondantes » à la limite.
Pourquoi les regex se bloquent : le backtracking exponentiel
La plupart des moteurs de regex (JavaScript, Python, Java, PHP, Ruby, Perl) utilisent un NFA basé sur le backtracking. Quand ils tombent sur (\d+)* — des « quantificateurs imbriqués » — le moteur ne sait pas combien de fois matcher, donc il essaie toutes les possibilités par force brute. Matcher (\d+)* sur 30 caractères comme 123...0 demande 2³⁰ = 1 milliard de découpages — chacun testé contre le reste de l’entrée, tous échouant avant que le moteur renvoie false.
Le point clé : chaque caractère supplémentaire dans l’entrée double le nombre de tentatives. C’est la complexité exponentielle. 30 caractères = 1 milliard de tentatives (~30 secondes sur un CPU moderne). 60 caractères = 10¹⁸ tentatives (~30 ans).
Bonne nouvelle : il existe une méthode pour identifier et corriger ces cas. Le testeur de regex de Piick affiche les compteurs en temps réel, ce qui vous permet de détecter « cette regex va exploser sur une longue entrée » avant qu’elle n’arrive en production.
3 cas réels en production
Cas 1 : la regex d’email fait tomber le login
Un site e-commerce utilise ce validateur d’email :
^([a-zA-Z0-9_.-])+@([a-zA-Z0-9_.-])+.([a-zA-Z]{2,4})+$
Ça a l’air correct. Le problème est le + final — « n’importe quel nombre de segments de TLD ». Quand un utilisateur saisit un email légal mais inhabituellement long comme john.doe@a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p, le moteur commence à découper le TLD de façon exponentielle et sature le CPU en quelques secondes.
Correction : supprimez le + final et matchez un seul segment de TLD.
Cas 2 : la regex de parsing de logs tue le cluster ELK
Une équipe utilise cette regex pour parser les logs nginx :
^(\d+.\d+.\d+.\d+) - - [([^]]+)] ”([A-Z]+) (.+?) HTTP/[\d.]+” (\d+) (\d+)
Sur des logs standard, ça tourne en millisecondes. Un jour, l’équipe ops importe des logs historiques de 2008 (format différent, espaces supplémentaires), le pattern .+? déclenche le backtracking, chaque ligne prend 5 secondes, tout le pipeline ELK se bloque, Kibana tombe en timeout sur tous les dashboards.
Cause racine : .+? est suivi de la méthode HTTP, mais la méthode à l’intérieur des guillemets n’est pas ancrée — le moteur ne sait pas « où s’arrêter ».
Correction : remplacez .+? par [^\s]+ — les URLs ne contiennent jamais d’espace, donc l’étendue du match se réduit à une taille raisonnable.
Cas 3 : la regex de force de mot de passe bloque l’inscription
Le formulaire d’inscription utilise :
^(?=.[a-z])(?=.[A-Z])(?=.\d)(?=.[@$!%*?&]).{8,}$
Quatre lookaheads positifs, chacun scannant l’entrée entière. Un mot de passe de 1000 caractères (test d’attaque par collage) déclenche 4 × 1000 scans — lent mais pas fatal.
La variante réellement mortelle :
^(?=.[a-z])(?=.[A-Z])(?=.\d).[@$!%*?&].*$
Elle condense les quatre lookaheads en « n’importe quoi + caractère spécial + n’importe quoi ». Un utilisateur tape aaaaaaaaaaaa! — beaucoup de a et un !. Le moteur essaie de couper à chaque position de a (« je coupe ici, puis je matche ! »), exponentiellement.
Correction : utilisez des classes de caractères au lieu de .*, ou n’utilisez pas de regex pour la force du mot de passe — utilisez un outil.
Comment repérer une regex qui va exploser
Impossible à voir à l’œil nu — ^(\w+\s?)*$ a l’air innocent, mais 30 caractères bloquent le moteur V8. Trois méthodes :
Méthode 1 : tester des entrées longues dans le testeur de regex de Piick
Le testeur de regex de Piick affiche le compteur de matches. Si vous voyez 100 000+ matches, c’est presque certainement du backtracking catastrophique. Une regex normale produit un nombre de matches proportionnel à la longueur de l’entrée.
Étapes : collez votre regex, injectez une longue chaîne qui ne matche volontairement pas (comme aaaa...!), observez :
- Retour en quelques secondes → sûr
- Bloque 5+ secondes → haut risque
- Gèle l’onglet → fermez immédiatement
Méthode 2 : analyse statique avec ReScue
ReScue (Université de Nanjing, article PLDI 2018) analyse statiquement la complexité du pire cas d’une regex. rescue analyze 'your-pattern-here' renvoie SAFE / VULNERABLE / UNKNOWN. UNKNOWN ne veut pas dire sûr — cela signifie seulement que l’outil n’a pas pu trancher ; cela peut quand même bloquer à l’exécution.
Méthode 3 : changer de moteur et ajouter des timeouts
Quelle que soit la sécurité apparente d’une regex, le code de production devrait utiliser des moteurs de regex en temps linéaire — RE2 de Google (require('re2') en Node.js, regexp standard de Go, pyre2 en Python). RE2 a une complexité structurelle en O(entrée × taille_regex), donc il n’explose jamais. Ajoutez un timeout en worker thread comme ceinture et bretelles : 100 ms puis kill.
2 façons de corriger
Méthode 1 : réécrire la regex, réduire les combinaisons
Idée centrale : remplacer des « limites floues » par des « limites explicites ».
// dangereuse ^(\w+\s?)$ // sûre : les espaces deviennent obligatoires ^(\w+\s)\w*$
La première autorise « n’importe quels mots, chacun avec un espace optionnel ». La seconde exige « [mot + espace] un nombre quelconque de fois », ce qui rend les espaces obligatoires. Même résultat matché, mais la première explore une infinité de découpages tandis que la seconde n’en a qu’un seul.
Règle empirique : quand vous voyez (\w+)*, (.+)*, (.*)* — « quantificateur à l’intérieur d’un groupe, avec un autre quantificateur à l’extérieur » — tirez la sonnette d’alarme. 99 % sont des pièges de backtracking.
Méthode 2 : groupes atomiques ou quantificateurs possessifs
JavaScript, Java et Python ne supportent pas nativement les groupes atomiques (?>...), mais on peut les simuler avec un lookahead + backreference :
// simule un groupe atomique : dit au moteur « ne backtrack pas ici » (?=(\w+))\1
Le lookahead (?=(\w+)) vérifie si \w+ matche à partir d’ici, stocke le résultat dans un groupe de capture ; ensuite \1 le référence et interdit au moteur de couper à l’intérieur.
La solution d’ingénierie la plus large : changer de moteur (méthode 3 ci-dessus). Compromis : RE2 ne supporte pas les backreferences ni la plupart des lookarounds. Validation de mot de passe, parsing d’URL — entièrement compatibles. Parsing de logs qui a besoin de références \1 — il faut quand même réécrire la regex.
Comment Piick vous aide à l’éviter
Le testeur de regex de Piick est plus qu’un vérificateur de matches — le compteur de matches est votre système d’alerte précoce : collez la regex, injectez une longue chaîne qui ne matche volontairement pas (comme aaaa...z), surveillez à la fois le compteur et le temps de réponse. Même ordre de grandeur que la longueur de l’entrée = sûr. Exponentiel = haut risque. Faites-le tourner sur chaque regex que vous écrivez — cinq secondes maintenant vous évitent une panne P0 plus tard.
Le backtracking catastrophique est la bombe invisible du monde dev — les tests passent, la production meurt, on ne peut pas le reproduire, et on rejette la faute sur « des saisies utilisateur bizarres ». Aujourd’hui, identifiez-le et corrigez-le. Ouvrez le testeur de regex de Piick, faites passer vos 5 regex les plus longues par le test d’alerte ci-dessus, et vous éviterez la page d’astreinte du trimestre prochain.