# Internals des WAF modernes : architecture, évasion et retour de production NyxR > Une analyse experte des WAF modernes, du parsing HTTP à la corrélation comportementale, avec les choix d'architecture de NyxR, une méthodologie red team et un mois de métriques issues de nyxr.app. Publié le 2026-07-22 | Mis à jour le 2026-07-22 | Tags: waf, web security, red team, blue team, owasp crs, nyxr https://xsec.fr/defensive/modern-waf-internals/ --- import WafTechnicalInfographic from '@shared/components/WafTechnicalInfographic.astro' import Callout from '@shared/components/Callout.astro' > Un **Web Application Firewall (WAF)**, ou pare-feu applicatif web, analyse le trafic du protocole **Hypertext Transfer Protocol (HTTP)** avant qu'il n'atteigne le serveur d'origine, c'est-à-dire le service applicatif final. Un WAF moderne est *stateful* : il conserve un état entre plusieurs événements afin de corréler une requête avec un client, une politique et un historique. Il doit aussi canonicaliser chaque message, donc réduire ses représentations équivalentes à une forme commune, exactement comme les composants en aval. Si le *proxy*, intermédiaire qui reçoit puis relaie la requête, le WAF et l'application ne donnent pas le même sens aux mêmes octets, le moteur de détection ne protège plus le message consommé par l'origine. Le 23 juin 2026, entre 19 h 35 et 19 h 40, heure de Paris (UTC+2), la passerelle de sécurité NyxR a enregistré sur mon application [nyxr.app](https://nyxr.app) 3 119 requêtes provenant d'un scanner. En cinq minutes, celui-ci a parcouru 2 100 **Uniform Resource Identifiers (URI)**, les identifiants de ressources visés par les requêtes. Le **data plane**, ensemble des composants qui interprètent et décident chaque requête synchroniquement avant l'envoi de la réponse, en a bloqué 3 117, soit 99,94 %, avec une latence de 0 ms au 50e percentile ou médiane (`p50`) et au 95e percentile (`p95`). **ModSecurity**, moteur WAF open source, a produit en parallèle 3 121 transactions d'audit associées à la règle `913100` de l'**Open Worldwide Application Security Project Core Rule Set (OWASP CRS)**, un jeu de règles générique qui reconnaît ici le `User-Agent`, chaîne par laquelle un client déclare son logiciel, d'un scanner connu. Reconnaître un scanner connu n'est pourtant que la partie visible du résultat. Pour qu'un tel blocage soit fiable, un WAF moderne doit aussi reconstruire la bonne adresse client derrière les proxys, refuser les messages HTTP ambigus, exécuter les contrôles dans le bon ordre, appliquer la politique localement et journaliser la décision sans compter deux fois la même transaction. Une erreur dans l'une de ces couches peut créer un *bypass*, donc une requête qui échappe au contrôle, ou un faux positif, c'est-à-dire un trafic légitime classé à tort comme malveillant. J'ai conçu NyxR autour de cette chaîne. Cet article expose les choix d'architecture qui en découlent, de la délimitation du message à la décision finale, puis vérifie leur comportement sur un mois de métriques produites par mon application nyxr.app. ## Architecture et chaîne de confiance Le modèle historique du WAF est celui d'un filtre : la requête entre, des expressions la comparent à des signatures, puis elle est acceptée ou rejetée. Ce modèle explique un moteur de règles, mais pas un système moderne. Une requête réelle traverse plusieurs interprètes. Le client sérialise un message. Un **Content Delivery Network (CDN)**, réseau distribué de caches et de proxys, peut le recevoir en premier ; un répartiteur de charge (*load balancer*) distribue ensuite le trafic entre plusieurs serveurs, tandis qu'un proxy inverse (*reverse proxy*) relaie la requête au nom de l'origine. Chacun peut la décoder, la normaliser, la convertir de HTTP/2 vers HTTP/1.1, ajouter des en-têtes et réutiliser une connexion vers le *backend*, service situé en aval. Le WAF construit ensuite ses propres collections de variables. Enfin, le framework applicatif décode l'**Uniform Resource Locator (URL)**, l'adresse de la ressource demandée, ainsi que les cookies, formulaires et corps structurés. **JavaScript Object Notation (JSON)** est un format texte de données structurées ; **Extensible Markup Language (XML)** est un langage à balises ; un corps *multipart* sépare plusieurs parties, notamment champs et fichiers, par des délimiteurs. La sécurité dépend de l'accord entre tous ces interprètes. Une **Request for Comments (RFC)** est une spécification technique publiée dans l'écosystème de l'**Internet Engineering Task Force (IETF)**, organisme qui standardise les protocoles Internet. Le [RFC 9110](https://www.rfc-editor.org/info/rfc9110) sépare la sémantique HTTP de sa représentation. Le [RFC 9112](https://www.rfc-editor.org/info/rfc9112) définit le cadrage HTTP/1.1 et avertit que des interprétations différentes facilitent le *request smuggling*, attaque qui fait délimiter le même flux de requêtes différemment par deux composants successifs. HTTP/2 et HTTP/3 déplacent ce cadrage dans des trames binaires, mais les conversions vers un backend HTTP/1.1 réintroduisent une frontière d'interprétation. Un WAF moderne doit donc répondre à cinq questions distinctes : 1. **Où la requête commence-t-elle et où finit-elle ?** C'est le problème de cadrage et de *parsing*, l'analyse structurale du message. 2. **Qui ou quoi l'a probablement envoyée ?** C'est le problème d'identité et de confiance dans les proxies. 3. **Que signifie son contenu après transformations ?** C'est le problème de canonicalisation et de détection. 4. **Que révèle sa relation avec les requêtes précédentes ?** C'est le problème temporel et comportemental. 5. **Quelle action reste sûre en cas de panne ?** C'est le problème d'architecture et de mode dégradé. La détection n'est qu'une étape. Le parsing, la normalisation, l'enrichissement, la décision, l'*enforcement*, application effective de l'action, la journalisation et l'apprentissage doivent rester séparés. Ces responsabilités n'ont ni la même échéance ni le même mode de panne. Les organiser en plans distincts permet de garder la décision courante locale tout en faisant évoluer la politique et l'analyse sur des cycles plus longs. **Trois plans, pas un monolithe.** Dans une architecture de WAF moderne, un *plan* est un groupe logique de composants organisé autour d'une responsabilité et d'un rythme d'exécution ; il ne correspond pas nécessairement à une machine distincte. Le **data plane** prend la décision courante : il termine la connexion, établit le contexte client, applique les contrôles locaux, inspecte le contenu, puis bloque ou relaie la requête. Le **control plane** gère la publication de la politique. Il reçoit l'*état désiré*, description déclarative de la politique attendue par l'opérateur, le valide, le versionne et le transforme en configuration exécutable sans intervenir dans la requête courante. Le **plan d'observation** reçoit une copie des décisions après leur application, conserve l'historique, corrèle les événements et propose des évolutions. Une proposition ne peut influencer une requête future qu'après être repassée par le control plane ; elle ne réécrit jamais la décision déjà prise. J'ai conçu NyxR selon cette séparation, comme une *Unified Web Security Gateway*, passerelle auto-hébergée qui regroupe proxy inverse, WAF et contrôles comportementaux devant plusieurs services. Le chemin critique, ou *hot path*, est la partie du data plane composée de toutes les opérations qu'une requête doit attendre avant d'obtenir sa réponse. Une base ou un service distant placé sur ce chemin devient donc une dépendance directe de latence et de disponibilité. Dans l'implémentation NyxR, le hot path combine **NGINX**, serveur web et proxy inverse, **OpenResty**, distribution de NGINX qui embarque le langage de script **Lua** via **LuaJIT**, moteur qui compile Lua à l'exécution, puis libModSecurity v3 et OWASP CRS. **Transport Layer Security (TLS)** chiffre la connexion entre deux pairs ; son extension **Server Name Indication (SNI)** annonce le nom d'hôte visé pendant la négociation afin de sélectionner le bon certificat et le bon service. Hors de ce chemin, le control plane écrit l'état désiré dans **PostgreSQL**, base de données relationnelle, et le code **TypeScript**, variante typée de JavaScript, construit un *bundle*, c'est-à-dire un paquet complet de configuration prêt à activer. Le plan d'observation transporte les événements via **Redis Streams**, journal ordonné en mémoire fourni par Redis, puis un *scorer* convertit des signaux pondérés en score. Une règle en mode *shadow*, ou mode d'observation, calcule et journalise ce qu'elle aurait décidé sans modifier la réponse envoyée au client. Les indicateurs de **Cyber Threat Intelligence (CTI)** sont des données de menace connues, par exemple des adresses associées à une infrastructure malveillante. Cette séparation répond à une contrainte fondamentale : la décision synchrone ne doit pas appeler PostgreSQL, un modèle d'intelligence artificielle ou un *datastore analytique*, stockage optimisé pour agréger et interroger des événements, à chaque requête. Une **Access Control List (ACL)**, ou liste de contrôle d'accès, est un ensemble de règles qui autorisent ou refusent un trafic selon des critères explicites. Une adresse **Internet Protocol (IP)** identifie une interface sur un réseau IP ; la notation **Classless Inter-Domain Routing (CIDR)** associe une adresse à une longueur de préfixe pour désigner un réseau entier, par exemple `192.0.2.0/24`. Dans un WAF moderne, les ACL nécessaires à la requête doivent être évaluées localement dans le hot path afin de bloquer ou laisser passer sans dépendre d'une base distante. Un *snapshot* est une copie cohérente et figée à un instant donné. NyxR applique ce principe aux ACL, aux snapshots de réputation, aux compteurs et aux autres politiques de décision. Une panne du control plane laisse ainsi le dernier bundle valide en service. Cette autonomie locale n'est réelle que si une politique peut être publiée sans exposer de configuration partielle. Le mécanisme d'activation doit donc préserver un dernier état valide et rendre chaque changement vérifiable avant qu'il n'atteigne le data plane. Le *config builder*, composant qui compile l'état désiré en fichiers exécutables, ne modifie pas le fichier actif ligne par ligne. Il rend un bundle complet, effectue des contrôles structurels et `nginx -t`, calcule une somme de contrôle (*checksum*) pour détecter toute différence d'octets, écrit une nouvelle version puis bascule le lien actif atomiquement, donc sans état intermédiaire visible. Il recharge enfin la passerelle et exécute un contrôle de santé (*health check*) ; une activation malsaine restaure la version précédente. Cette architecture apporte trois garanties utiles à un pentester : - la version de politique qui a servi une requête peut être reliée à la décision ; - un contournement du control plane n'implique pas un contournement immédiat du data plane ; - une dépendance analytique indisponible ne devrait pas modifier silencieusement la grammaire HTTP acceptée. Elle apporte aussi une question de test : le dernier bundle connu est-il réellement autonome, ou une dépendance distante cachée peut-elle transformer une panne en *fail-open*, mode dans lequel le trafic est autorisé lorsque le contrôle échoue ? À l'inverse, un système *fail-closed* refuse le trafic pendant cette panne. ## Du socket à la décision La séparation des plans indique qui publie la politique, qui décide et qui observe. Elle ne suffit pas à montrer dans quel ordre le data plane acquiert les informations nécessaires à une décision ; il faut donc suivre une requête depuis la connexion brute jusqu'à l'action finale. Une *socket* réseau est l'extrémité logicielle par laquelle le serveur reçoit une connexion. À partir de cette socket, une requête ne doit pas être pensée comme une chaîne passée à une fonction `is_malicious()` : elle évolue à mesure que de nouvelles informations deviennent disponibles. Une empreinte (*fingerprint*) résume des traits techniques du client sans prétendre l'identifier ; le *rate limiting* borne le nombre de requêtes dans une fenêtre de temps ; un *challenge* exige une preuve supplémentaire, souvent l'exécution d'un navigateur, avant de poursuivre. ### Construire le contexte de décision **1. Terminaison TLS et cadrage HTTP.** La terminaison TLS déchiffre la connexion sur la passerelle et révèle le message HTTP. Le **ClientHello** est le premier message envoyé par le client pendant la négociation TLS ; ses versions, suites cryptographiques et extensions exposent des traits de sa pile réseau. Cette terminaison fixe aussi une frontière de confiance : un attaquant qui peut joindre directement l'origine contourne tous les contrôles situés à l'*edge*, la couche périmétrique exposée à Internet. L'authentification **mutual TLS (mTLS)** vérifie par certificat le serveur et le client ; avec la restriction réseau ou un secret d'origine, elle empêche qu'un client non autorisé contourne la passerelle. Avant d'inspecter un *payload*, le contenu utile transporté par la requête, la passerelle doit rejeter les messages dont la longueur ou la structure pourrait être comprise différemment en aval. Les travaux [HTTP Desync Attacks](https://i.blackhat.com/USA-19/Wednesday/us-19-Kettle-HTTP-Desync-Attacks-Smashing-Into-The-Cell-Next-Door.pdf), [The HTTP Garden](https://arxiv.org/abs/2405.17737) et [WAFFLED](https://arxiv.org/abs/2503.10846) montrent que les divergences de parseur ne sont pas un cas académique. WAFFLED rapporte 1 207 contournements confirmés sur cinq WAF largement utilisés en modifiant des éléments non malveillants de JSON, XML et multipart. Un WAF moderne doit rejeter les ambiguïtés de cadrage avant ses décisions de confiance. Dans NyxR, j'ai placé un garde de désynchronisation local à cet endroit précis. Son rôle n'est pas de détecter une injection **Structured Query Language (SQL)**, où une entrée modifie la structure d'une requête adressée à une base de données, mais de refuser une requête avant qu'un proxy et une origine puissent lui donner deux significations. Le cadrage établit une représentation non ambiguë de la requête, mais pas l'identité réseau à laquelle rattacher la décision. Une fois le message délimité, la chaîne de confiance doit déterminer quelle adresse peut alimenter la réputation, les quotas et les bannissements. **2. Adresse IP réelle et chaîne de confiance.** L'en-tête **`X-Forwarded-For` (XFF)** transporte la chaîne d'adresses déclarées par les proxys traversés ; il n'est pas une identité, mais une affirmation. Le module NGINX [ngx_http_realip_module](https://nginx.org/en/docs/http/ngx_http_realip_module.html) ne la transforme en adresse client qu'après configuration de sources de confiance avec `set_real_ip_from`. Si tout Internet est approuvé, l'attaquant choisit sa réputation, son pays, sa clé de limitation et parfois sa capacité à contourner une *allowlist*, liste explicite de sources autorisées. Le bon invariant pour un WAF moderne est : seul un saut réseau (*hop*) explicitement approuvé peut remplacer l'adresse reçue sur la socket. Dans NyxR, je pars d'un ensemble de confiance vide, je publie les CIDR des proxys approuvés dans le bundle et j'applique un garde supplémentaire. **IPv4** et **IPv6** sont les deux versions déployées du protocole IP, avec des espaces d'adressage et parfois des routes différentes. Le **protocole PROXY** transporte hors des en-têtes HTTP l'adresse de connexion originale entre deux proxys qui se font confiance. Pour un red teamer, le test porte sur chacun de ces chemins, mais aussi sur l'accès direct, le CDN, le répartiteur de charge et le réseau d'administration. Une identité réseau suffisamment fiable rend les décisions indexées par adresse exploitables. Le data plane peut alors appliquer les contrôles locaux les moins coûteux avant de réserver l'inspection profonde aux requêtes restantes. **3. Garde-fous locaux à faible coût.** Les ACL, la géolocalisation, les indicateurs CTI, les bannissements actifs et certaines réputations peuvent être évalués avant l'inspection profonde du corps. Cela économise du temps de processeur, de la mémoire et de la bande passante vers l'origine. Cette priorité n'est sûre que si les dérogations sont explicites : une allowlist doit préciser quels contrôles elle contourne et lesquels restent obligatoires. Ces garde-fous écartent les sources ou contextes déjà connus, mais une requête isolée peut rester banale. La corrélation comportementale ajoute alors le temps, les erreurs et la séquence des routes afin de reconnaître une automatisation répartie sur plusieurs transactions. **4. Comportement et empreinte client.** L'IP, l'**Autonomous System Number (ASN)**, numéro du réseau qui annonce l'adresse sur Internet, la famille de User-Agent, l'ordre des en-têtes, la version HTTP, le résultat des challenges, le débit, les réponses HTTP `404`, les erreurs d'authentification et les routes parcourues ont des durées de vie différentes. Leur corrélation permet de traiter un robot distribué qu'aucun payload isolé ne trahit. Le comportement décrit comment un client agit dans le temps, pas ce que contient la transaction courante. ModSecurity et CRS apportent cette inspection du message normalisé en recherchant des motifs dans ses variables, ses en-têtes et son corps. **5. Inspection ModSecurity et CRS.** Le WAF parse les variables disponibles, applique des transformations déterministes, exécute des règles et accumule un score d'anomalie. Ce score est la somme des points ajoutés par les règles qui correspondent à la transaction, pas une probabilité d'attaque. Les limites de corps, de profondeur JSON et de nombre d'arguments font partie du contrôle : une erreur de parsing ignorée crée une zone que l'origine comprend mais que le WAF ne comprend pas. L'inspection produit des correspondances et un score, mais elle ne choisit pas seule l'effet visible par le client. La dernière étape confronte ces signaux à la politique afin de décider entre transmission, limitation, challenge, bannissement ou refus. **6. Friction et décision finale.** Un blocage n'est pas toujours la meilleure réponse. Un challenge peut séparer un navigateur interactif d'un client automatisé. Une limite de débit peut protéger un *endpoint*, combinaison d'une méthode HTTP et d'une route applicative, sans prétendre que chaque requête est malveillante. Un bannissement temporaire peut amortir une séquence. Le journal final doit conserver séparément l'action réellement appliquée et la raison candidate. Ce parcours situe le moteur de règles parmi plusieurs contrôles indépendants. Pour interpréter correctement ses résultats, il reste à comprendre quand une règle s'exécute, quelles variables elle voit et comment ses points deviennent éventuellement une action disruptive. ### Interpréter ModSecurity et OWASP CRS ModSecurity v3 exécute les règles dans cinq phases documentées : en-têtes de requête, corps de requête, en-têtes de réponse, corps de réponse et journalisation. Les données sont cumulatives. Une règle de phase 1 ne voit pas encore tous les arguments du corps ; une règle de phase 5 ne peut plus bloquer la transaction. La [référence ModSecurity v3](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-%28v3.x%29) décrit ces contrats et les différences de support par rapport à v2. Les familles montrées ci-dessous couvrent notamment la **Local File Inclusion (LFI)**, inclusion d'un fichier local, la **Remote Code Execution (RCE)**, exécution de code à distance, le **Cross-Site Scripting (XSS)**, injection de code exécuté dans le navigateur, et la **SQL injection (SQLi)**. Le **Paranoia Level (PL)** choisit la sensibilité des règles activées. Le mode `DetectionOnly` conserve les détections sans appliquer d'action disruptive, tandis que le mode `On` autorise notamment le refus de la requête. Les phases déterminent les données accessibles à chaque règle, mais pas la manière dont plusieurs correspondances sont agrégées. Le score d'anomalie fournit cette couche de composition avant l'évaluation du seuil de blocage. **Le score d'anomalie n'est pas une probabilité.** CRS utilise une détection collaborative. Une règle critique ajoute typiquement 5 points, une erreur 4, un avertissement 3 et une notice 2. Les règles de détection sont séparées de l'évaluation de blocage. La documentation [Anomaly Scoring](https://coreruleset.org/docs/2-how-crs-works/2-1-anomaly_scoring/) explique que `REQUEST-949-BLOCKING-EVALUATION.conf` compare le score entrant total au seuil configuré. Un score de 10 ne signifie ni 10 % de risque ni une attaque deux fois plus probable qu'un score de 5. Il signifie que des règles ont ajouté dix points selon une convention de politique. La bonne preuve conserve au minimum : - les identifiants de règles qui ont matché ; - les variables et transformations examinées ; - le niveau de paranoïa exécuté ; - le seuil d'anomalie ; - le mode `DetectionOnly` ou `On` ; - l'action finale du gateway. Le score explique comment les règles contribuent à une transaction, mais sa sensibilité dépend encore des règles activées et de leur contexte. Le niveau de paranoïa élargit la couverture, tandis que les exclusions retirent précisément les cibles légitimes qui provoquent des faux positifs. **Niveau de paranoïa et exclusions.** Les [niveaux de paranoïa](https://coreruleset.org/docs/2-how-crs-works/2-2-paranoia_levels/) activent des règles de plus en plus sensibles de PL1 à PL4. Augmenter le seuil pour compenser un PL plus élevé peut masquer une règle critique isolée. La stratégie recommandée est de choisir le PL selon le risque, garder un seuil bas, observer les faux positifs puis écrire des exclusions étroites. Une exclusion sûre vise une règle, une URI et si possible un paramètre. Désactiver toute la famille SQLi parce qu'un champ Markdown contient le mot `select` résout le faux positif en supprimant la protection. L'exemple générique suivant ne reproduit pas la configuration de NyxR : `REQUEST_URI` désigne la route reçue, `ARGS` la collection des paramètres et `REQBODY_ERROR` l'indicateur d'échec du parseur de corps. ```apache title="crs-tuning.conf" {2-6,9-13} # Retirer uniquement le paramètre "content" de la règle sur cet endpoint. SecRule REQUEST_URI "@beginsWith /api/articles" \ "id:100100,phase:1,pass,nolog,ctl:ruleRemoveTargetById=942100;ARGS:content" # Conserver une erreur de parsing comme événement bloquant. SecRule REQBODY_ERROR "!@eq 0" \ "id:100110,phase:2,t:none,log,deny,status:400,msg:'Request body parsing failed'" # Publier explicitement la politique de scoring de la transaction. SecAction "id:100120,phase:1,pass,nolog,t:none,\ setvar:tx.blocking_paranoia_level=2,\ setvar:tx.inbound_anomaly_score_threshold=5" ``` Les identifiants `100100` à `100120` sont réservés ici à l'exemple. En production, ils doivent appartenir à une plage locale documentée et ne pas entrer en collision avec CRS. Les exclusions règlent le périmètre des variables que le moteur sait analyser ; elles ne donnent aucune visibilité sur un corps tronqué, surdimensionné ou impossible à parser. Les plafonds de ressources et la politique appliquée lorsqu'ils sont dépassés constituent donc une frontière de sécurité distincte. Les directives ModSecurity `SecRequestBodyLimit`, `SecRequestBodyNoFilesLimit`, `SecArgumentsLimit` et `SecRequestBodyJsonDepthLimit` bornent respectivement la taille du corps, sa partie hors fichiers, le nombre d'arguments et la profondeur JSON. Si la politique traite seulement le début d'un corps surdimensionné puis transmet le reste, l'application peut recevoir une partie que le WAF n'a jamais inspectée. Si elle rejette systématiquement, elle peut casser des téléversements légitimes. Une limite doit donc être définie par route et testée contre les formats réellement acceptés. Un WAF moderne doit versionner ces plafonds et les adapter aux formats réellement acceptés par chaque service. NyxR applique ce modèle par service ; un échec `REQBODY_ERROR` produit une réponse HTTP `400 Bad Request`, donc le refus explicite d'un corps que le moteur n'a pas su parser. Dans l'extrait de production que j'ai constitué, 12 transactions provenant de trois adresses ont déclenché le contrôle local d'erreur de parsing. Le volume est faible, mais sa signification est forte : NyxR a distingué « contenu non reconnu » de « contenu reconnu et jugé bénin ». ## Corréler l'identité sans ralentir le data plane La chaîne précédente suffit à décider une transaction, mais son adresse IP ne suffit pas à relier durablement plusieurs requêtes au même logiciel ou au même comportement. La corrélation doit donc enrichir l'identité avec des traits protocolaires et temporels sans transformer le hot path en dépendance analytique. ### Empreinte et comportement La **Network Address Translation (NAT)** fait partager une adresse IP publique à plusieurs clients ; les proxys et sorties cloud produisent un effet similaire. Une adresse IP reste donc un point réseau observé, pas une identité humaine. Avant d'envoyer le premier octet HTTP, le client TLS émet un **ClientHello**. Ce message propose notamment les versions TLS, les suites cryptographiques, les algorithmes de signature, les groupes de clés et des extensions comme SNI ou **Application-Layer Protocol Negotiation (ALPN)**, mécanisme qui sélectionne notamment HTTP/1.1 ou HTTP/2 ; le serveur choisit ensuite les paramètres compatibles dans sa réponse. La passerelle qui termine TLS voit donc une description structurée des capacités et des choix de la bibliothèque cliente avant de voir la requête applicative. **JA4** transforme cette description en une empreinte `a_b_c` documentée par le [projet FoxIO](https://github.com/FoxIO-LLC/ja4). La section `a`, lisible, encode le type de transport, la version TLS, la présence d'un nom SNI, le nombre de suites et d'extensions, puis un résumé de l'ALPN. La section `b` contient les 12 premiers caractères hexadécimaux du hachage de la liste triée des suites cryptographiques. La section `c` fait de même pour les extensions triées, hors SNI et ALPN, complétées par les algorithmes de signature. **Secure Hash Algorithm 256 bits (SHA-256)** est ici une fonction de hachage qui réduit une liste de taille variable à un résumé déterministe ; JA4 n'en conserve que 12 caractères pour la compacité. Le tri retire l'effet de l'ordre aléatoire des suites et extensions. Les valeurs **Generate Random Extensions And Sustain Extensibility (GREASE)**, valeurs factices injectées pour empêcher les implémentations TLS de se figer sur une liste connue, sont ignorées. JA4 est donc plus stable qu'une concaténation brute, mais ce n'est ni une authentification ni un identifiant unique : une bibliothèque partagée produit la même empreinte sur de nombreux hôtes, une pile configurable peut imiter un navigateur, une mise à jour peut changer l'empreinte et une terminaison TLS amont peut masquer le ClientHello original. Un WAF moderne peut compléter JA4 avec des traits HTTP afin de ne pas dépendre d'un identifiant unique. Dans NyxR, j'ai choisi un composite fondé sur l'ordre des en-têtes, la famille de User-Agent, la version HTTP, les traits manquants et l'ALPN. Une mesure de cohérence compare les déclarations du client à sa forme protocolaire. L'objectif n'est pas de prouver qu'un client est malveillant, mais de rendre coûteuse la rotation d'un seul attribut. Une empreinte stabilise le contexte technique du client, mais elle ne démontre aucune intention malveillante. Le score comportemental ajoute la dimension temporelle en accumulant seulement des événements explicables, puis en réduisant leur poids lorsqu'ils ne se répètent plus. Le score comportemental serveur est cumulatif et décroît avec une demi-vie, durée au bout de laquelle le poids d'un signal est divisé par deux. Seuls les événements porteurs d'un signal positif touchent Redis. Une requête bénigne ne soustrait pas artificiellement des points ; le silence fait décroître le score. Les signaux ModSecurity et du journal d'accès sont disjoints afin de ne pas compter deux fois la même transaction. Le score est un accumulateur de preuves pondérées. Il peut devenir très élevé pendant une séquence d'automatisation soutenue. Il ne doit jamais être affiché comme un pourcentage de malveillance. Le score applique des règles déterministes déjà publiées ; il ne découvre pas seul de nouveaux profils ni ne valide leur innocuité. L'apprentissage peut analyser l'historique pour proposer une évolution, à condition de rester asynchrone et de repasser par le control plane avant toute mise en application. ### Apprendre hors du chemin critique Un système adaptatif dangereux applique directement une sortie probabiliste sur chaque requête. Il ajoute une dépendance réseau, rend la latence non déterministe, expose le data plane aux quotas du fournisseur et transforme une indisponibilité du modèle en changement de sécurité. Une architecture adaptative moderne doit séparer le moteur de scénarios local, qui reconnaît des séquences déterministes, le scorer asynchrone, qui agrège les événements après la requête, et le service de verdict, qui ajoute un avis de classification. J'ai matérialisé cette séparation dans NyxR : une règle apprise commence en mode d'observation, où son résultat est journalisé sans influencer la réponse, puis la nouvelle politique traverse la chaîne de construction et de validation avant de pouvoir devenir active. Dans NyxR, j'ai conçu le service de verdict comme un enrichissement asynchrone qui ne possède jamais la décision courante. Les données de nyxr.app en fournissent un exemple concret : pendant les dernières 24 heures de la fenêtre, ce service a dépassé son délai de réponse, le scorer a journalisé le repli vers le seuil local et la passerelle est restée saine. Le fait utile n'est donc pas la disponibilité du modèle d'intelligence artificielle, mais la décision déterministe qui reste appliquée lorsqu'il ne répond pas. Sur les 482 bans durables créés dans la fenêtre : - 275 proviennent du scorer comportemental et 207 de scénarios edge adoptés dans l'historique ; - ils concernent 316 cibles distinctes ; - le score médian au ban est 42, le 95e percentile 103 et le maximum 4 224 ; - l'enrichissement stocké conclut `ban` pour 434, `monitor` pour 46 et `allow` pour 2. Ces deux verdicts `allow` ne rendent pas les autres décisions fausses. Ils prouvent qu'un classifieur, composant qui affecte une observation à une catégorie, et un seuil déterministe peuvent diverger. Le système doit alors montrer la priorité de politique, la durée de la dérogation, la décision finale et la possibilité d'annulation. ## Un mois de trafic réel issu de nyxr.app Les mécanismes précédents définissent ce que le système devrait séparer : représentation, identité, signal, décision et apprentissage. Les données de production permettent maintenant de vérifier si ces frontières restent visibles dans les événements, sans confondre plusieurs projections d'une même requête. ### Lire les résultats sans double compter J'ai extrait ces agrégats de mon application [nyxr.app](https://nyxr.app). Ils couvrent un mois d'activité, du 21 juin au 22 juillet 2026. La table relationnelle `security_events` conserve deux projections, c'est-à-dire deux représentations du même trafic destinées à des usages distincts : - 145 996 requêtes journalisées par la passerelle avec leur résultat final ; - 20 878 transactions d'audit ModSecurity, qui portent le match de règle principal et le contexte WAF. Le total physique est 166 874 enregistrements, mais il ne représente pas 166 874 requêtes uniques. Une même requête peut produire un enregistrement dans chaque projection. Ce périmètre fixe les dénominateurs et évite d'additionner le journal de la passerelle à l'audit WAF. Une fois cette distinction établie, la projection du data plane permet de mesurer les actions effectivement renvoyées aux clients. **Résultats de la passerelle.** | Action finale | Requêtes journalisées | Part des 145 996 | |---|---:|---:| | Autorisée | 120 278 | 82,38 % | | Bloquée | 17 320 | 11,86 % | | Challenge présenté | 4 909 | 3,36 % | | Client déjà banni | 2 837 | 1,94 % | | Rate limited | 652 | 0,45 % | Sur cette fenêtre, NyxR a journalisé 8 638 adresses réelles distinctes sur 23 services. Une adresse n'est pas un acteur et un service n'est pas une population homogène. Ces cardinalités servent à décrire la surface, pas à attribuer des attaques. Les raisons explicites montrent plusieurs contrôles actifs : 4 212 blocages CTI sur 762 adresses, 3 952 blocages géographiques sur 1 716 adresses et 97 blocages issus d'un *honeypot*, service leurre conçu pour attirer et signaler les interactions suspectes, sur 74 adresses. Cependant, 8 994 enregistrements marqués `blocked` n'ont pas de `block_reason` spécifique, principalement dans les données antérieures à l'instrumentation complète. Ce trou interdit d'utiliser la colonne comme vérité exhaustive sur toute la fenêtre. Les actions finales indiquent ce qui a été appliqué, pas quelle source de renseignement a contribué à un blocage CTI. La provenance relie chaque indicateur aux listes qui le portaient afin de comparer leur couverture réelle, leur fraîcheur et leur recouvrement. **Quelles listes CTI ont réellement déclenché ?** La *provenance CTI* associe l'indicateur qui a bloqué une requête aux listes publiques qui le contenaient. Elle distingue ainsi la taille d'un catalogue de son efficacité mesurée. Sur les 4 212 requêtes bloquées par CTI, 3 968 restent attribuables au snapshot de provenance courant ; 244 ne peuvent plus lui être rattachées, notamment parce qu'elles précèdent l'enregistrement systématique de la clé exacte ou concernent un indicateur sorti depuis d'une liste rotative. | Source et logique de construction | Blocages attribués | Adresses source distinctes | Indicateurs dans le snapshot actif | |---|---:|---:|---:| | [Data-Shield IPv4 Blocklist, variante Critical](https://github.com/duggytuxy/Data-Shield_IPv4_Blocklist), registre issu de sondes mondiales avec une rétention glissante annoncée de 15 jours | 3 731, soit 88,6 % des blocages CTI | 558 | 78 699 | | [Blocklist.de, liste « all attacks »](https://blog.blocklist.de/en/export.html), adresses signalées pour des attaques contre les serveurs participants pendant les 48 dernières heures | 946, soit 22,5 % | 91 | 28 795 | | [IPsum niveau 3+](https://github.com/stamparm/ipsum), agrégat quotidien qui ne conserve ici que les adresses présentes dans au moins trois listes publiques | 811, soit 19,3 % | 117 | 18 178 | Ces comptes ne sont pas exclusifs : une requête est attribuée à chaque liste qui porte l'indicateur correspondant. Les trois lignes totalisent donc 5 488 attributions pour 4 212 blocages uniques. Ce chevauchement est informatif. Data-Shield apporte la couverture la plus large et domine le trafic arrêté ; Blocklist.de privilégie une fenêtre très fraîche ; IPsum apporte un critère de consensus inter-listes. Le retour opérationnel est de conserver cette provenance par indicateur, d'expirer les sources selon leur propre cadence et de mesurer les faux positifs par liste. Additionner les compteurs ou juger une source sur sa seule taille produirait une conclusion erronée. La CTI explique des décisions prises à partir d'indicateurs déjà connus ; elle ne décrit pas les séquences reconnues après plusieurs événements. Les dossiers de bans comportementaux montrent quels signaux temporels ou applicatifs ont réellement contribué à cette autre voie de décision. **Quels signaux comportementaux ont le plus contribué ?** Le tableau suivant porte sur les preuves enregistrées dans les 482 bans durables, pas sur des seuils privés de déploiement ni sur toutes les requêtes brutes. Un même ban peut contenir plusieurs signaux ; « bans contenant le signal » compte chaque ban une seule fois, tandis que « occurrences » conserve les répétitions présentes dans son dossier de preuves. | Famille de signal | Mécanisme enregistré | Bans contenant le signal | Occurrences de preuve | |---|---|---:|---:| | Sonde de chemin sensible | La route demandée ressemble à une recherche de métadonnées de gestion de versions, de fichiers de configuration, de sauvegardes ou d'interfaces d'administration. Le signal décrit une catégorie ; il ne révèle pas la liste exacte de chemins du déploiement. | 205, soit 42,5 % | 207 | | LFI / RFI | Les familles [CRS 930](https://github.com/coreruleset/coreruleset/blob/main/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf) et [CRS 931](https://github.com/coreruleset/coreruleset/blob/main/rules/REQUEST-931-APPLICATION-ATTACK-RFI.conf) ont reconnu une forme d'inclusion de fichier local ou distant. C'est la preuve d'un motif d'attaque, pas celle qu'un fichier a été lu. | 106, soit 22,0 % | 106 | | Anomalie de protocole HTTP | Les familles [CRS 920](https://github.com/coreruleset/coreruleset/blob/main/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf) et [CRS 921](https://github.com/coreruleset/coreruleset/blob/main/rules/REQUEST-921-PROTOCOL-ATTACK.conf) signalent un cadrage, une méthode ou des en-têtes incohérents. Ce signal est utile contre les parseurs divergents, mais doit être testé avec les clients et proxies légitimes. | 78, soit 16,2 % | 86 | Au total, 387 bans sur 482, soit 80,3 %, contiennent au moins une de ces trois familles. Deux seulement en combinent deux dans le même dossier de preuves. Le fait dominant n'est donc pas une exploitation applicative confirmée : c'est une reconnaissance automatisée centrée sur les chemins, les inclusions de fichiers et les écarts de protocole. Le meilleur gain défensif consiste à arrêter ces séquences localement, puis à conserver les matches WAF et le contexte temporel pour expliquer le ban. Les signaux de ban décrivent une accumulation dans le temps, tandis que l'audit ModSecurity conserve la vue transactionnelle du moteur de règles. Classer son identifiant principal permet de comparer les familles détectées, mais pas de transformer chaque correspondance en blocage effectif. **Familles de règles enregistrées.** Le tableau classe chaque transaction ModSecurity par son `rule_id` principal, identifiant numérique de la règle retenue comme cause principale. Il ne recompte pas les règles secondaires présentes dans le tableau d'audit. | Famille principale | Transactions d'audit | Adresses distinctes | Action WAF `blocked` | Score max | |---|---:|---:|---:|---:| | Protocole HTTP, policy et cadrage, règles 920 à 922 | 7 145 | 288 | 3 371 | 970 | | LFI, chemins et fichiers restreints, règles 930 | 6 667 | 203 | 5 545 | 30 | | Détection de scanners, règles 913 | 3 397 | 3 | 3 397 | 5 | | SQLi, règles 942 | 1 558 | 55 | 38 | 20 | | RCE et injection de commandes, règles 932 | 1 454 | 79 | 61 | 55 | | Fuite d'information en réponse, règles 950 à 954 | 466 | 155 | 40 | 4 | | XSS, règles 941 | 110 | 14 | 29 | 25 | | Injection PHP, langage exécuté côté serveur, règles 933 | 34 | 6 | 0 | 5 | | Remote File Inclusion (RFI), inclusion de fichier distant, règles 931 | 20 | 11 | 7 | 20 | | Erreur de parsing de corps, contrôle local | 12 | 3 | 5 | 5 | Le contraste entre 1 558 transactions SQLi et seulement 38 actions WAF `blocked` montre pourquoi « match » ne signifie pas automatiquement « requête refusée ». Certaines transactions sont en détection, d'autres peuvent avoir un score sous le seuil ou provenir d'une politique différente. L'analyse correcte nécessite le mode et la version de configuration. Ces agrégats révèlent les familles dominantes, mais effacent l'ordre et la cadence des requêtes. Une fenêtre de campagne restitue cette dimension temporelle, puis permet de confronter l'identité calculée et le coût de la décision au comportement réellement observé. ### Campagne, empreintes et latence **Cas réel : une campagne de fuzzing d'URI.** Cette campagne de *fuzzing d'URI*, génération automatisée de nombreux chemins pour découvrir des routes, fichiers ou comportements inattendus, a produit 3 119 requêtes journalisées par la passerelle en 300 secondes, soit 10,4 requêtes par seconde en moyenne. Il ne s'agit pas d'un **Distributed Denial of Service (DDoS)** volumétrique, attaque distribuée qui cherche à épuiser une ressource par le volume, mais d'une exploration rapide et large de chemins applicatifs avec une signature de scanner et 2 100 URI distinctes. Les 3 117 refus locaux ont un p50 et un p95 de 0 ms dans le journal NGINX. Le maximum de la fenêtre est 477 ms. L'intérêt n'est pas de revendiquer « zéro latence WAF », car la résolution de l'horloge et le chemin de journalisation limitent cette mesure. L'intérêt est que la décision n'attend pas l'origine ni un moteur distant. Cette campagne enseigne aussi le risque du double comptage : les 3 121 audits de règle 913100 et les 3 119 requêtes journalisées par la passerelle décrivent deux projections presque jumelles, pas 6 240 requêtes. La séquence et la signature du scanner caractérisent cette campagne, mais elles n'établissent pas une identité client durable. Les empreintes permettent d'étudier la continuité technique entre plusieurs adresses, à condition de conserver leurs limites et leur rôle expérimental. **Empreintes mesurées et limite de JA4.** 65 446 requêtes journalisées possèdent un fingerprint composite non vide, soit 44,83 % des accès de la passerelle. Elles couvrent 935 fingerprints et 6 104 adresses journalisées. La cohérence vaut `0` sur 9 047 enregistrements et `1` sur 56 399. 224 fingerprints apparaissent derrière au moins cinq adresses. Parmi eux, 67 ont une proportion d'actions non autorisées supérieure ou égale à 50 %. Le fingerprint le plus distribué apparaît derrière 1 235 adresses. Cela ne prouve pas l'existence d'un botnet : une pile commune, un CDN ou un navigateur populaire peut produire une forte dispersion. Le *learner*, composant qui regroupe les observations récurrentes pour proposer des profils, a créé 24 profils classés `distributed_attack` et 12 campagnes actives représentant 1 071 requêtes et 277 appartenances d'IP cumulées. Ces comptes sont des hypothèses de *clustering*, regroupement algorithmique d'observations similaires, pas des identités d'attaquants. Le résultat le plus important est négatif : **aucune valeur JA4 non vide n'est présente dans cette fenêtre**, malgré le champ et le code d'instrumentation. Je ne peux donc pas attribuer les décisions de cette fenêtre à JA4. La terminaison TLS, la version du *runtime*, c'est-à-dire du logiciel réellement exécuté, et la chaîne d'ingestion, parcours qui transporte le signal jusqu'au stockage, doivent être vérifiées avant d'évaluer l'efficacité de ce signal. Enfin, 2 218 requêtes journalisées ont `block_reason=fingerprint` alors que leur action finale est `allowed`. Le fingerprint a donc été calculé et journalisé comme signal expérimental, mais il n'a pas participé au blocage de ces requêtes. Cette distinction évite qu'un tableau de bord transforme une expérimentation en protection active imaginaire. La couverture des empreintes indique quels signaux étaient disponibles, pas le coût de leur évaluation ni celui des autres contrôles. Les percentiles par action séparent donc la réponse locale du temps passé auprès de l'origine avant toute conclusion sur la performance du WAF. **Coût et latence.** Les percentiles calculés sur les requêtes journalisées par la passerelle séparent les actions. Le `p99` est la valeur sous laquelle se trouvent 99 % des mesures, et expose une traîne de latence que la médiane masque. | Action | p50 | p95 | p99 | |---|---:|---:|---:| | Autorisée | 71 ms | 707 ms | 2 641,7 ms | | Bloquée localement | 0 ms | 0 ms | 49 ms | | Challenge | 4 ms | 42 ms | 63 ms | | Client déjà banni | 0 ms | 0 ms | 2 ms | | Rate limited | 2 ms | 11 ms | 325,5 ms | La latence autorisée inclut le temps de l'origine ; elle ne mesure pas le coût du WAF seul. La latence bloquée mesure surtout la production locale de la réponse. Un *benchmark* est une mesure comparative réalisée sous des conditions contrôlées : il doit ici comparer un même endpoint, un même payload et une même origine avec WAF désactivé, `DetectionOnly` puis `On`, tout en séparant p50, p95, p99, temps processeur, allocations mémoire, taille du corps et nombre de règles évaluées. ## Ce que le WAF peut et ne peut pas garantir Les métriques précédentes mesurent des décisions et des signaux observables ; elles n'étendent pas pour autant les connaissances du WAF sur l'état interne de l'application. Définir cette limite évite de présenter une détection syntaxique ou comportementale comme une preuve d'exploitation ou d'autorisation correcte. Un WAF peut très bien reconnaître une tentative de path traversal sans savoir si le fichier visé existe. Il peut détecter une forme SQLi sans connaître la requête SQL finalement construite. Il peut limiter un flux d'achat sans savoir si l'utilisateur possède légitimement cent comptes. Il peut observer un identifiant d'objet sans savoir si le principal authentifié a le droit métier de le lire. Une **Application Programming Interface (API)** expose des opérations et des données applicatives à d'autres logiciels au moyen d'un contrat. L'[OWASP API Security Top 10 2023](https://owasp.org/API-Security/editions/2023/en/0x11-t10/) place les défauts d'autorisation sur les objets et les fonctions parmi les risques majeurs. Un WAF générique ne dispose pas du graphe d'autorisations de l'application : celle-ci doit encore vérifier l'identité authentifiée, l'objet, l'action et le contexte. | Classe de risque | WAF utile | Contrôle propriétaire réel | |---|---|---| | Injection syntaxique | Parser, transformations, CRS, schéma de contenu | Requêtes paramétrées, encodage de sortie, validation typée | | Broken Object Level Authorization (BOLA), aussi appelé Insecure Direct Object Reference (IDOR) : accès à l'objet d'un autre utilisateur | Anomalies de route et de volume | Autorisation objet dans l'application | | Autorisation de fonction cassée : appel d'une opération interdite au rôle courant | Protection de routes connues | Role-Based Access Control (RBAC), droits par rôle, ou Attribute-Based Access Control (ABAC), droits calculés depuis des attributs, côté service | | Abus de workflow | Rate limit, session, challenge, séquence | Invariants métier et contrôles transactionnels | | Server-Side Request Forgery (SSRF) : faire émettre au serveur une requête vers une destination choisie | Détection de formes et allowlist d'URL | Résolveur, politique réseau sortante et validation après Domain Name System (DNS), service qui traduit un nom en adresse IP | | Request smuggling | Parsing strict et normalisation edge | Accord de cadrage sur toute la chaîne | | Compromission de l'origine | Blocage du trafic direct | Pare-feu, mTLS, secret d'origine, segmentation | Le WAF est donc un contrôle compensatoire, c'est-à-dire une couche qui réduit un risque sans corriger sa cause dans l'application, et un point d'observation puissant. Il n'est pas un substitut au code sûr. ## Évaluer un WAF comme un red teamer Les limites de responsabilité précédentes déterminent la méthode de test : l'évaluation doit comparer les représentations vues par la périphérie, le WAF et l'origine, puis attribuer chaque résultat au composant qui possède réellement le contrôle. Une *red team* simule un attaquant dans un périmètre autorisé afin d'évaluer les contrôles et leur détection. Son évaluation ne demande pas seulement « quel payload passe ? » : elle cherche quelle représentation voit chaque composant, quelle décision a été prise et quelle preuve reste après la tentative. ### Protocole d'évaluation reproductible **1. Cartographier la chaîne réelle.** Relever : - la résolution DNS et les CDN éventuels ; - les protocoles acceptés côté client et côté origine ; - les downgrades HTTP/2 ou HTTP/3 vers HTTP/1.1 ; - les headers de forwarding et les CIDR de confiance ; - la possibilité de joindre l'origine directement ; - la version du moteur, de CRS et de la politique par hôte virtuel (*vhost*), configuration qui associe un nom de domaine à un service. Une réponse différente sur l'IP d'origine et sur le domaine public est déjà une conclusion d'architecture. La cartographie identifie les interprètes et les frontières de confiance, mais elle ne prouve pas qu'ils donnent le même sens aux mêmes octets. Les tests de différentiels doivent donc faire varier une seule dimension de représentation à la fois et comparer les sorties de chaque couche. **2. Tester les différentiels de parseur.** Faire varier un axe à la fois : - duplication de `Content-Length`, en-tête qui annonce la taille du corps, combinaison avec `Transfer-Encoding`, qui décrit son codage de transfert, et espaces non canoniques ; - HTTP/2 vers HTTP/1.1, pseudo-en-têtes HTTP/2 préfixés par `:` et normalisation des noms ; - paramètres dupliqués, tableaux implicites et encodages répétés ; - `application/json`, `application/x-www-form-urlencoded`, qui encode un formulaire en paires clé-valeur, multipart et XML ; - profondeur, nombre d'arguments, fichiers et corps partiels ; - URL absolue, dot segments, slashs encodés et séparateurs propres au framework. Le résultat attendu n'est pas « alerte ». Il faut comparer ce que le WAF journalise à ce que l'application reçoit dans un lab instrumenté. Un *fuzzer différentiel* génère automatiquement des variantes d'entrée et cherche celles que deux parseurs interprètent différemment ; il est plus instructif qu'une longue liste statique de payloads. Un différentiel établit quelles représentations divergent, mais pas si une correspondance de règle a effectivement modifié la requête. La preuve doit ensuite séparer le parsing, la détection, le calcul du score, l'action appliquée et ce que l'origine a reçu. **3. Séparer détection, score et action.** Pour chaque test, enregistrer : | Dimension | Preuve minimale | |---|---| | Parsing | Processeur sélectionné, erreur éventuelle, variables produites | | Détection | Tous les rule IDs, targets, transformations et messages | | Scoring | Contributions par PL, score total et seuil | | Enforcement | Statut local, connexion fermée ou requête transmise | | Origine | Requête réellement reçue et réponse produite | | Observabilité | Access log, audit WAF, corrélation et version de config | Cette chaîne de preuve relie le contenu à l'action ; elle reste invalide si les compteurs et réputations utilisent une identité contrôlable par l'attaquant. Il faut donc tester séparément la reconstruction de l'adresse, les empreintes et les collisions entre plusieurs clients légitimes. **4. Tester l'identité comme une frontière de sécurité.** Envoyer des en-têtes de forwarding depuis une source non approuvée, traverser chaque proxy légitime, comparer IPv4 et IPv6, puis vérifier la clé réellement utilisée pour CTI, rate limiting et bans. Tester la rotation d'IP avec un fingerprint stable et la stabilité d'IP avec plusieurs clients légitimes. Un bon résultat peut être « le système refuse d'attribuer ». L'incertitude explicite est préférable à une fausse précision. Les tests d'identité couvrent la relation entre clients et clés de décision, mais une automatisation peut répartir son activité sous chaque seuil instantané. L'étape suivante étend l'analyse aux fenêtres temporelles, à la cardinalité des états et aux séquences distribuées. **5. Tester le temps et la distribution.** Les signatures voient une transaction. Les abus modernes se répartissent : - faible débit sur de nombreuses IP ; - rotation d'User-Agent avec un même comportement de routes ; - endpoints coûteux appelés sous le seuil global ; - échec de challenge distribué ; - séquences rares qui restent bénignes isolément. Mesurer les fenêtres, la demi-vie, la cardinalité des clés, la mémoire Redis, les collisions NAT et la récupération après expiration. Un contrôle peut rester exact sous une charge nominale et changer silencieusement de politique lorsqu'une dépendance tombe. Les essais de panne vérifient donc quels mécanismes restent locaux, lesquels passent en observation et quel mode dégradé devient réellement actif. **6. Tester les modes de panne.** Couper en environnement autorisé et contrôlé : Redis, le scorer, le learner, le service de verdict, PostgreSQL et le control plane. Pour chaque panne, documenter ce qui continue, ce qui passe en observation, ce qui autorise par défaut, ce qui refuse par défaut et ce que voit l'opérateur. Une panne analytique ne doit pas modifier le parsing. Une panne Redis ne doit pas désactiver silencieusement toute limite. Un bundle invalide ne doit jamais devenir actif. Une file d'événements pleine ne doit pas bloquer les *workers HTTP*, processus qui exécutent le traitement des requêtes. Les modes de panne établissent la résilience, mais pas la spécificité d'une détection. Chaque cas malveillant doit donc être rejoué avec un voisin bénin de même forme pour déterminer quelle propriété déclenche réellement le résultat. **7. Rejouer avec des contrôles négatifs.** Chaque payload malveillant doit avoir un voisin bénin de même forme : même `Content-Type`, en-tête qui déclare le format du corps, même taille, même nombre de paramètres et même endpoint. Ce voisin est un **contrôle négatif** : il ne contient pas la propriété malveillante et vérifie que cette propriété, plutôt que la forme générale, déclenche le résultat. Ces cas contrôlés ne deviennent comparables que si le lab conserve la configuration, les entrées et les sorties de chaque exécution. Une matrice reproductible transforme alors un contournement ponctuel en expérience vérifiable et falsifiable. **Construire un lab reproductible.** Le test suivant illustre une matrice minimale sans fournir de payload d'exploitation. Ce script de shell utilise **curl**, client en ligne de commande pour les protocoles réseau, afin d'envoyer le même document JSON à un endpoint de lab, comparer les modes du WAF et conserver les en-têtes de réponse. ```bash title="waf-matrix.sh" {5-10,14-18} set -euo pipefail target="https://lab.example.test/api/parse" artifact_dir="./artifacts/waf-$(date -u +%Y%m%dT%H%M%SZ)" mkdir -p "$artifact_dir" for case_name in baseline duplicate-key deep-json oversized-fields; do curl --http2 --silent --show-error \ --dump-header "$artifact_dir/$case_name.headers" \ --output "$artifact_dir/$case_name.body" \ --header 'Content-Type: application/json' \ --data-binary "@$case_name.json" \ --write-out '%{http_code} %{time_total}\n' \ "$target" | tee "$artifact_dir/$case_name.result" done sha256sum "$artifact_dir"/* > "$artifact_dir/SHA256SUMS" ``` `sha256sum` calcule ici une empreinte SHA-256 de chaque artefact ; toute modification ultérieure change cette valeur et devient détectable. La même exécution doit capturer côté serveur : version du bundle, mode WAF, PL, seuil, audit complet, journal de passerelle, requête normalisée reçue par l'origine et ressources consommées. Le test devient alors réfutable et reproductible. Le lab produit des preuves détaillées pour chaque couche. Ces observations peuvent ensuite être condensées en invariants d'architecture qui restent valables lorsque le payload, l'adresse ou la signature de l'outil change. **Dix invariants qui résistent à l'évasion.** 1. **Parser avant de classifier.** Un contenu que le WAF ne peut pas parser doit suivre une politique explicite. 2. **Réduire les interprètes.** Normaliser tôt, éviter les downgrades inutiles et aligner edge et origine. 3. **Établir l'identité avant les compteurs.** Une clé de rate limit basée sur une IP spoofable n'est pas un contrôle. 4. **Garder les décisions locales.** Le hot path ne doit pas attendre une base ou un modèle distant. 5. **Séparer signal, raison et action.** Une décision calculée uniquement pour observation ne doit pas apparaître comme un blocage effectif. 6. **Versionner la politique.** Une alerte sans version de configuration est difficile à reproduire. 7. **Préférer les exclusions étroites.** L'objectif est de retirer un faux positif, pas une famille de protection. 8. **Borner tous les états.** Corps, profondeur, arguments, cardinalités, files, historique et **Time To Live (TTL)**, durée après laquelle un état expire. 9. **Mesurer les faux négatifs et les faux positifs.** Un faux négatif est une attaque non détectée ; un taux de blocage seul récompense les systèmes inutilisables. 10. **Traiter le WAF comme une couche.** L'autorisation et les invariants métier restent dans l'application. ## Le modèle mental à retenir Le protocole précédent transforme des tests isolés en propriétés durables. Pris ensemble, ses invariants conduisent à considérer le WAF comme un système distribué qui ne voit qu'une représentation partielle de la requête et de son auteur. Le WAF moderne se comprend comme un système distribué partiellement observable. Le data plane reçoit une représentation de la requête, pas l'intention de l'opérateur. Il établit une frontière HTTP, reconstruit une identité probabiliste, applique une politique locale et émet une preuve. D'autres composants corrèlent cette preuve dans le temps, mais ne doivent pas réécrire rétroactivement ce qui s'est passé. Pour analyser une évasion, quatre questions suffisent souvent : 1. **Quelle représentation exacte le WAF a-t-il parsée ?** 2. **Quelle représentation l'origine a-t-elle finalement consommée ?** 3. **Quel signal a contribué à quelle décision, sous quelle version de politique ?** 4. **Quelles couches indépendantes restent actives si ce signal disparaît ?** Les résultats de production que je présente à partir de nyxr.app rendent ce modèle concret. NyxR a absorbé localement une campagne de fuzzing d'URI, calculé et journalisé un fingerprint sans l'utiliser pour bloquer, maintenu la passerelle lorsque le service de verdict a expiré et distingué une détection ModSecurity d'un blocage effectif. Ces faits rendent le système testable, ce qui est plus utile qu'une promesse de « protection intelligente ». Un WAF efficace n'est pas celui qui bloque le plus. C'est celui qui interprète les requêtes avec le moins d'ambiguïté, décide avec un état local borné, explique chaque action, apprend sans posséder le hot path et reconnaît ce que seule l'application peut autoriser. ## Références - IETF, [RFC 9110 : HTTP Semantics](https://www.rfc-editor.org/info/rfc9110), 2022. - IETF, [RFC 9112 : HTTP/1.1](https://www.rfc-editor.org/info/rfc9112), 2022. - IETF, [RFC 9113 : HTTP/2](https://www.rfc-editor.org/info/rfc9113), 2022. - IETF, [RFC 9114 : HTTP/3](https://www.rfc-editor.org/info/rfc9114), 2022. - OWASP ModSecurity, [Reference Manual v3.x](https://github.com/owasp-modsecurity/ModSecurity/wiki/Reference-Manual-%28v3.x%29). - OWASP Core Rule Set, [Anomaly Scoring](https://coreruleset.org/docs/2-how-crs-works/2-1-anomaly_scoring/), [Paranoia Levels](https://coreruleset.org/docs/2-how-crs-works/2-2-paranoia_levels/) et [Rule Exclusions](https://coreruleset.org/docs/2-how-crs-works/2-3-false-positives-and-tuning/). - OWASP Core Rule Set, [versions publiées de CRS](https://github.com/coreruleset/coreruleset/releases). - NGINX, [ngx_http_realip_module](https://nginx.org/en/docs/http/ngx_http_realip_module.html). - OpenResty, [lua-nginx-module](https://github.com/openresty/lua-nginx-module), phases Lua et dictionnaires partagés. - OWASP, [API Security Top 10 2023](https://owasp.org/API-Security/editions/2023/en/0x11-t10/). - Kettle, [HTTP Desync Attacks: Request Smuggling Reborn](https://i.blackhat.com/USA-19/Wednesday/us-19-Kettle-HTTP-Desync-Attacks-Smashing-Into-The-Cell-Next-Door.pdf), Black Hat USA. - Kallus et al., [The HTTP Garden: Discovering Parsing Vulnerabilities in HTTP/1.1 Implementations by Differential Fuzzing of Request Streams](https://arxiv.org/abs/2405.17737), 2024. - Akhavani et al., [WAFFLED: Exploiting Parsing Discrepancies to Bypass Web Application Firewalls](https://arxiv.org/abs/2503.10846), 2025. - Demetrio et al., [WAF-A-MoLE: Evading Web Application Firewalls through Adversarial Machine Learning](https://arxiv.org/abs/2001.01952), 2020. - FoxIO, [JA4+ Network Fingerprinting](https://github.com/FoxIO-LLC/ja4).