Pourquoi les modèles de regex avec rétrogradation peuvent faire planter silencieusement votre serveur
Découvrez comment la correspondance gourmande et le rebond catastrophique peuvent transformer une expression régulière apparemment correcte en une panne majeure qui paralyse le processeur, ainsi que comment repérer ce risque.
Sources: The Regex That Looked Fine in Testing and Hung the Server in Production
Aucun bug n’a été introduit au sens habituel. La regex elle-même était valide et correspondait exactement à ce qu’elle devait identifier. Cependant, elle présentait une caractéristique cachée que la plupart des développeurs n’apprennent jamais à détecter ; pour la comprendre, il faut aller au-delà des symboles et saisir ce qu’une expression régulière fait réellement lorsqu’elle est exécutée, ce que la plupart des tutoriels omettent en se contentant d’expliquer des éléments tels que \d et +.
Une expression régulière est un petit diagramme de flux, pas une chaîne de symboles
Considérons un modèle destiné à valider un code de produit composé de deux lettres majuscules suivies de quatre chiffres :
const productCode = /^[A-Z]{2}\d{4}$/;
console.log(productCode.test("LM4821")); // true
console.log(productCode.test("LM482")); // false
Il est naturel de voir cela comme une description abrégée de la forme d’une chaîne. Un modèle mental plus adapté, qui s’avère utile par la suite, consiste à le considérer comme un mini-diagramme de flux : on commence par exiger exactement deux lettres majuscules, puis exactement quatre chiffres, avant d’exiger la fin de l’entrée. Le moteur parcourt l’entrée caractère par caractère, tentant de suivre un itinéraire à travers ce diagramme depuis le nœud de départ jusqu’au nœud de fin. Lorsqu’il atteint la fin avec succès, c’est une correspondance. S’il se heurte à un obstacle alors qu’il reste encore une branche non essayée quelque part derrière lui, il revient en arrière et essaie cette autre branche à la place.
Ce pas en arrière — revenir pour tester une branche alternative — est connu sous le nom de backtracking, et c’est sans doute le concept le plus important dans les expressions régulières. C’est ce qui leur confère leur flexibilité, et c’est aussi précisément ce qui a causé l’incident de production décrit ci-dessus.
Les groupes transforment « Est-ce que ça correspond ? » en « Qu’y a-t-il réellement à l’intérieur ? »
Appeler .test() ne renvoie que true ou false. En encadrant des parties d’un motif entre parenthèses, on peut capturer individuellement des fragments du texte correspondant, ce qui permet aux expressions régulières d’être véritablement utiles pour le parsing plutôt que simplement pour la validation :
const logPattern = /^\[(\w+)\] (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}) (.+)$/;
const line = "[ERROR] 2026-08-15T10:22:01 Failed to connect to database";
const match = logPattern.exec(line);
console.log(match[1]); // "ERROR"
console.log(match[2]); // "2026-08-15T10:22:01"
console.log(match[3]); // "Failed to connect to database"
Chaque section entre parenthèses du motif devient une entrée distincte dans le tableau de résultats, classée selon l’emplacement de sa parenthèse d’ouverture dans le motif. Cela transforme une simple vérification de succès/échec — afin de confirmer qu’une ligne a la forme d’une entrée de journal — en quelque chose de bien plus utile : disposer du niveau de gravité, de la date et heure, ainsi que du corps du message en tant que trois valeurs distinctes prêtes à être utilisées. C’est cette capacité qui explique pourquoi les expressions régulières sont si fréquemment utilisées dans le traitement des journaux, l’analyse des configurations, et dans toute situation où un texte peu structuré doit être converti en quelque chose que le programme peut traiter directement.
La correspondance gourmande récupère plus que prévu
Cette erreur suivante est commise presque par tous les développeurs au moins une fois. Imaginez un cas où il est nécessaire d’éliminer les balises HTML légères d’un champ de texte :
const html = "<b>Featured</b> item, <i>on sale</i> now";
console.log(html.replace(/<.*>/g, ""));
// "Featured item, on sale now" — wait, that actually looks right...
const html2 = "<b>Sale</b> ends <i>Friday</i>";
console.log(html2.replace(/<.*>/g, ""));
// "" — everything vanished
La deuxième chaîne révèle la faille. Par défaut, .* est gourmand : il tente d’absorber autant de caractères que possible, ne les restituant que si le reste du motif l’exige absolument. Comme cette chaîne contient deux balises distinctes, le motif <.*> ne s’arrête pas à la première balise de fermeture </b>. Il continue d’absorber tout ce qui suit — au-delà de ends, jusqu’à la dernière > de la chaîne, à l’intérieur de </i> — car un seul motif gourmand .*> est pleinement satisfait dès qu’il atteint la dernière > disponible dans le texte, peu importe à quelle distance elle se trouve.
console.log(html2.replace(/<.*?>/g, ""));
// "Sale ends Friday"
Placer un ? juste après le * change son comportement de « gourmand » à « paresseux » : le moteur cherche désormais à correspondre au plus petit nombre possible de caractères, ne s’étendant que lorsqu’il n’a pas d’autre moyen de satisfaire ce qui suit. Grâce à ce changement, chaque séquence <...> se ferme au premier > rencontré plutôt qu’au dernier de la chaîne, ce qui permet de supprimer les balises une par une comme prévu. Cette différence d’un seul caractère, * contre *?, explique en silence une grande partie des bugs où les expressions régulières suppriment bien plus de texte que prévu.
Le bug qui a vraiment fait planter le serveur
Considérez une version simplifiée du schéma responsable de cette pause de onze secondes : une vérification d’identifiant visant à exiger un ou plusieurs groupes de caractères alphabétiques, éventuellement séparés, suivis d’un signe @.
const badPattern = /^(\w+)+@/;
console.log(badPattern.test("valid_user@example.com")); // true, fast
console.log(badPattern.test("this_is_a_fairly_long_username_with_no_at_sign"));
// hangs, sometimes for a very long time, depending on length
Le problème réside dans la répétition imbriquée : (\w+)+, c’est-à-dire une séquence d’un ou plusieurs caractères de mot, elle-même enfermée dans un groupe qui se répète une ou plusieurs fois. Lorsque la chaîne d’entrée contient réellement un @, le moteur trouve une correspondance presque immédiatement. Mais si l’on lui fournit une chaîne ne contenant aucun @, le moteur ne peut pas abandonner tout de suite ; il doit démontrer de manière exhaustive qu’aucun arrangement possible ne fonctionne. Or, le nombre d’arrangements à vérifier est énorme : le + intérieur peut saisir un caractère tandis que le groupe extérieur se répète sur le reste, ou il peut saisir deux caractères et laisser le groupe extérieur se répéter sur ce qui reste, et ainsi de suite — chaque séparation possible est testée. Le moteur revient en arrière à travers chacune de ces combinaisons avant d’être prêt à déclarer échec, et chaque caractère supplémentaire double approximativement le nombre de combinaisons.
const fixedPattern = /^\w+@/;
Lorsque l’on comprend la cause racine, la solution semble presque trop simple : la répétition imbriquée n’était jamais nécessaire à l’origine. \w+ seul signifie exactement la même chose que (\w+)+ pour tout entrée censée fonctionner correctement, mais sans le risque de rebond exponentiel qui se cache derrière. Cette défaillance porte un nom bien connu : rebond catastrophique. Il faut surveiller les cas où un opérateur de répétition est appliqué à un groupe qui contient déjà son propre opérateur de répétition ; des motifs tels que (x+)+, (x*)+ ou (x+)* sont toutes des variantes du même piège sous-jacent.
Que cela vous apprend-il concrètement à surveiller
Vous n’avez pas besoin de mémoriser toutes les formes de motifs dangereux qui existent. Ce qui compte, c’est d’assimiler une vérité fondamentale : un moteur de regex ne traite pas un motif instantanément, il recherche une correspondance, chemin par chemin, en revenant en arrière et en essayant un autre chemin chaque fois que le précédent échoue. Les motifs ordinaires offrent si peu de chemins à explorer que cette recherche se termine avant même que vous ne remarquiez un retard. Le véritable danger apparaît surtout avec des répétitions imbriquées et superposées, où le nombre de chemins à vérifier ne croît pas simplement de manière linéaire, mais s’accroît exponentiellement à chaque caractère supplémentaire d’entrée qui ne se comporte pas comme prévu. Testez donc vos regex non seulement sur les chaînes que vous pensez correspondre, mais aussi sur de longues chaînes créées délibérément pour échouer. Toute entrée que votre suite de tests n’a jamais envisagé d’essayer est précisément celle qui finira par apparaître en production.