En octobre 2026, deux semaines ont suffi pour rappeler une vérité que l'on préfère oublier : la sécurité d'une application ne vaut jamais plus que celle du moteur qui l'exécute. Après PHP, c'est Node.js qui a vu une faille de son cœur transformer une donnée anodine en exécution de commande. CVE-2026-111497, surnommée Node2Shell, est une écriture hors limites dans le décodeur Brotli embarqué dans le runtime. Elle ne vit pas dans le code que vous écrivez, mais sous lui, et c'est précisément ce qui la rend dangereuse. Cet article décrit le mécanisme en détail, montre où la faille se cache dans une pile Node réelle, donne les commandes pour savoir si vous êtes exposé, et déroule la correction étape par étape.
Une faille de runtime, pas de code applicatif
Le réflexe, devant une exécution de code à distance, est de chercher la faute dans l'application : une entrée mal filtrée, une désérialisation imprudente, un appel à eval. Node2Shell n'est rien de tout cela. Le défaut est dans le décodeur Brotli que Node.js embarque pour la compression, un composant que la quasi-totalité des applications utilisent sans jamais le nommer.
La conséquence est systémique. Une application dont le code est revu, testé, irréprochable, reste vulnérable tant que son runtime n'est pas à jour. Il n'y a pas de ligne à corriger dans le dépôt : il y a une version à changer. C'est la même famille de risque que PHP2Shell la semaine précédente, et la même discipline de défense en découle. Pour la lignée côté PHP, voir l'analyse PHP2Shell (CVE-2026-109618).
Le mécanisme : un tampon de huit octets qui déborde
Brotli est un algorithme de compression. Son décodeur, en mode flux, lit les octets compressés par morceaux et reconstruit la donnée d'origine au fur et à mesure. Pour cela, il tient un petit tampon interne où il accumule les octets d'un bloc avant de les interpréter.
L'avis Node.js décrit la cause en une phrase : un changement de bloc invalide était rapporté comme un besoin temporaire de données supplémentaires au lieu d'une erreur de format permanente. Autrement dit, quand le flux présentait une transition de bloc malformée, le décodeur ne disait pas « ce flux est corrompu, j'arrête ». Il disait « il me manque des octets, donne-m'en encore », et il continuait à écrire les octets reçus dans un tampon interne de huit octets, au-delà de sa capacité.
Flux Brotli normal : [ bloc A ][ switch valide ][ bloc B ]
↓ décodeur écrit dans son tampon, borné, OK
Flux Brotli forgé : [ bloc A ][ switch INVALIDE ][ octets attaquant... ]
↓ décodeur croit manquer de données
↓ continue d'écrire dans le tampon de 8 octets
↓ écrit AU-DELÀ → écriture hors limites (OOB write)C'est une écriture hors limites, de classe CWE-787, et c'est la variété de bug mémoire parmi les plus exploitables, pour une raison simple : l'attaquant ne se contente pas de lire au-delà d'une zone (ce qui fuit de la mémoire), il écrit dedans. Selon ce que ce débordement vient corrompre dans la mémoire adjacente, le résultat va du plantage (déni de service) à la divulgation de mémoire du processus, jusqu'à l'exécution de code à distance si l'attaquant parvient à réécrire une structure qui contrôle le flot d'exécution.
Le chemin complet d'un OOB write jusqu'à une RCE fiable sur un runtime moderne reste un travail d'expert : il faut façonner le tas pour que le débordement tombe sur une structure utile, contourner les protections mémoire, puis détourner un pointeur. Mais le point essentiel pour un défenseur est ailleurs : le déclencheur, lui, est trivial. Il suffit qu'une donnée Brotli fabriquée atteigne le décodeur. Toute la difficulté est du côté de l'attaquant qui veut une RCE propre ; du côté de la victime, la porte s'ouvre avec une seule requête bien formée.
node:zlib et Brotli : où la faille vit dans votre pile
Node.js expose Brotli par le module natif node:zlib. Le décodeur vulnérable est atteint dès qu'une de ces API reçoit des octets Brotli d'origine externe :
import { brotliDecompressSync, createBrotliDecompress } from "node:zlib";
// 1. Décompression directe d'un buffer reçu
const clair = brotliDecompressSync(bufferRecu); // bufferRecu vient du réseau ?
// 2. Flux de décompression (le cas "streaming" où vit la faille)
req.pipe(createBrotliDecompress()).pipe(destination);Mais le piège n'est pas là où on le regarde. La plupart des applications n'appellent jamais brotliDecompress explicitement. Elles déclenchent le décodeur indirectement, par trois chemins beaucoup plus courants :
// A. Une route qui accepte un corps compressé par le client.
// Beaucoup de frameworks décompressent automatiquement selon
// l'en-tête Content-Encoding de la requête entrante.
app.use(express.json()); // + middleware de décompression qui gère "br"
// B. Un appel fetch dont la RÉPONSE arrive compressée en Brotli.
// C'est le cas le plus sous-estimé : la donnée ne vient pas de
// votre utilisateur, mais d'une API tierce que vous interrogez.
const r = await fetch("https://api.partenaire.example/data");
const data = await r.json(); // si la réponse est en Content-Encoding: br,
// le runtime la décompresse pour vous
// C. Une bibliothèque qui décompresse pour vous sans que ce soit visible
// (client HTTP, cache, proxy, lecteur d'archives).Le cas B est le plus sournois. Une application qui appelle une API externe en lui faisant confiance décompresse sa réponse sans y penser. Si ce service tiers est compromis, ou si un attaquant peut se placer sur le chemin, il renvoie une réponse Brotli forgée et atteint votre décodeur. C'est le fetch sortant, pas seulement l'entrée utilisateur, qui ouvre la porte. Et seule la mise à jour du runtime couvre ce cas : aucune validation applicative ne s'interpose, parce que la décompression a lieu avant que votre code ne voie la réponse.
Un point à écarter tout de suite pour lever une confusion fréquente : le paquet npm nommé brotli n'est pas le composant vulnérable. La faille est dans le décodeur natif de Node.js lui-même, celui de node:zlib. Changer, retirer ou épingler ce paquet npm ne corrige rien.
Suis-je affecté ? Les vérifications concrètes
Trois questions, dans l'ordre. La première suffit souvent à trancher.
1. Quelle version de Node.js tourne réellement en production ?
# En local, dans le conteneur, sur le serveur : la vraie version qui exécute
node --version
# Exposé par le runtime lui-même, utile dans un endpoint de diagnostic
node -e 'console.log(process.version, process.versions.modules)'Comparez à la table ci-dessous. Toute version inférieure au correctif de sa branche est exposée ; toute branche non maintenue (20.x et antérieures) l'est aussi, sans correctif à attendre.
2. Où mon code ou mes dépendances décompressent-ils du Brotli ?
# Usage direct de l'API Brotli de node:zlib, dans le code et les dépendances
grep -rniE "brotliDecompress|createBrotliDecompress|BROTLI_DECODE|constants\.BROTLI" \
--include=*.js --include=*.ts --include=*.mjs . node_modules 2>/dev/null | head
# fetch / clients HTTP susceptibles de recevoir une réponse Brotli
grep -rniE "fetch\(|axios|got\(|undici|node-fetch" \
--include=*.js --include=*.ts . | head3. Ma surface accepte-t-elle du Brotli de l'extérieur ?
Une route qui lit un corps de requête compressé, un en-tête Accept-Encoding: br négocié sur un appel sortant, un proxy ou un cache qui stocke du Brotli : chacun est un point d'entrée. Une seule de ces trois cases cochée justifie la mise à jour immédiate, sans attendre de confirmer l'exploitabilité, parce que le coût du correctif est nul comparé au coût d'une RCE.
Versions affectées et correctifs
| Branche Node.js | Statut | Version corrigée |
|---|---|---|
| 26.x | Affectée | 26.12.1 |
| 24.x | Affectée | 24.22.1 |
| 22.x (LTS) | Affectée | 22.23.4 |
| 20.x et antérieures | Hors support, pas de correctif | Migrer vers 22.x ou 24.x |
| Bun | Affecté | 1.4.4 |
Les trois correctifs ont été publiés le 21 octobre 2026, ou peu après. L'avis officiel ne fait pas de correspondance CVE par CVE : il renvoie aux notes de publication de chaque version, qui regroupent l'ensemble des neuf failles du lot.
Corriger, étape par étape
La correction est une montée de version, mais une montée de version se gouverne. Quatre gestes, du plus urgent au plus durable.
1. Mettre à jour le runtime. Dans l'image de base, pas seulement en local.
# Épingler une version CORRIGÉE, pas un tag mouvant comme "22" ou "lts"
FROM node:22.23.4-bookworm-slim2. Verrouiller la version minimale dans le projet, pour qu'un environnement en retard échoue bruyamment au lieu de tourner vulnérable.
{
"engines": {
"node": ">=22.23.4 <23 || >=24.22.1 <25 || >=26.12.1"
}
}3. Poser une barrière en CI, pour qu'aucun build ne parte sur un runtime exposé.
# Échoue si la version qui construit est antérieure au correctif de sa branche
node -e '
const [maj, min, pat] = process.versions.node.split(".").map(Number);
const ok =
(maj === 22 && (min > 23 || (min === 23 && pat >= 4))) ||
(maj === 24 && (min > 22 || (min === 22 && pat >= 1))) ||
(maj === 26 && (min > 12 || (min === 12 && pat >= 1))) ||
(maj === 23 || maj === 25);
if (!ok) { console.error("Node vulnérable à CVE-2026-111497:", process.version); process.exit(1); }
console.log("Node OK:", process.version);
'4. Quitter les branches non maintenues. Si un service tourne encore en 20.x, aucun patch ne viendra. La seule réponse est la migration vers une branche corrigée. Un composant hors support est une dette de sécurité même quand le code applicatif est sain, parce que la prochaine faille d'interpréteur ne sera pas corrigée pour vous.
Ce qu'il ne faut pas faire : poser une règle de pare-feu applicatif sur les octets Brotli bruts en espérant filtrer les charges malveillantes. Le pare-feu inspecte un flux compressé dont il ne connaît pas le contenu, et le décodeur est atteint avant lui. Un filtre placé avant la décompression est aveugle. On supprime la primitive en corrigeant le moteur, on ne la tamise pas en amont.
Le reste du lot d'octobre 2026
Node2Shell n'est pas seule. L'avis du 21 octobre 2026 corrige neuf CVE, et s'arrêter à Brotli laisserait des portes ouvertes :
| CVE | Sévérité | En une ligne |
|---|---|---|
| CVE-2026-111497 | high | OOB write du décodeur Brotli, menant à une RCE (Node2Shell) |
| CVE-2026-105483 | high | Contournement de hostname TLS via des chiffres en pleine chasse |
| CVE-2026-106069 | high | Contournement de hostname TLS via résolution IP permissive |
| CVE-2026-105482 | high | Troncature de noms contenant un octet NUL dans dns.resolve |
| CVE-2026-105481 | medium | Socket keep-alive traitant une réponse écrite avant la requête |
| CVE-2026-106072 | medium | Validation de certificat laxiste sur hostnames IPv4 non canoniques |
| CVE-2026-106074 | medium | Caches d'entropie de randomUUID/randomInt figés dans les snapshots |
| CVE-2026-106070 | low | ZipFile renvoyant des octets contrôlés après échec de validation (26.x) |
| CVE-2026-106071 | low | Sur-autorisation du modèle de permissions par règle joker (26.x) |
| CVE-2026-106073 | low | Contournement du modèle de permissions via fs.realpathSync |
Les deux contournements de hostname TLS classés « high » méritent une mention : un certificat qui passe pour un autre hôte ouvre la voie à des interceptions, et ces failles touchent la confiance réseau, pas seulement la mémoire. La mise à jour du runtime les corrige toutes d'un coup, ce qui est l'argument décisif pour ne pas se contenter de « patcher Brotli ».
Se protéger, en synthèse
- Mettre à jour Node.js vers 22.23.4, 24.22.1 ou 26.12.1 : c'est le seul correctif réel, et il couvre aussi le cas des réponses
fetch. - Épingler la version corrigée dans l'image de base et dans
engines, pour qu'un retard échoue au lieu de tourner vulnérable. - Quitter une branche non maintenue (20.x et antérieures) : il n'y aura pas de patch.
- Valider la donnée décompressée, jamais les octets compressés : la frontière de confiance est le point d'interprétation, pas le point d'entrée.
- Inventorier où l'application décompresse du Brotli d'origine externe,
fetchsortant compris, pour mesurer l'exposition réelle plutôt que la supposer. - Ne pas confondre le paquet npm
brotliavec le décodeur natif denode:zlib: seul ce dernier est en cause.
La discipline est celle de toute faille de composant partagé : on supprime la cause en mettant à jour le moteur, on ne tamise pas le symptôme en amont. C'est le raisonnement qu'un ingénieur sécurité applicative doit tenir face à un runtime, et c'est exactement ce que la famille 2Shell d'octobre 2026 enseigne.
Chronologie
- 21 octobre 2026 : Node.js publie son lot de correctifs de sécurité d'octobre et l'avis « Node.js Brotli decoder out-of-bounds write permits remote code execution (CVE-2026-111497) », classé « high ». Les versions 22.23.4, 24.22.1 et 26.12.1 sont publiées le jour même ou peu après.
- 21 octobre 2026 : le même lot corrige huit autres CVE (contournements de vérification de hostname TLS, troncature DNS sur octet NUL, failles du modèle de permissions), dont la plus haute sévérité est « high ».
- 22-23 octobre 2026 : les éditeurs de sécurité (Qualys, Cycode, Vercel, Datadog) publient leurs analyses ; le nom informel « Node2Shell » se répand. Une démonstration d'exécution de code en laboratoire sur Node.js 24.22.0 est rapportée.
- 26 octobre 2026 : aucune exploitation dans la nature confirmée, faille absente du catalogue KEV de la CISA.
Qui a trouvé la faille
CVE-2026-111497 a été rapportée par les chercheurs d'OpenAI Codex Security, Khai Tran et Sylvie Mayer, dans le cadre d'un exercice de red teaming assisté par IA, puis divulguée au projet Brotli et aux projets en aval dont Node.js. Le correctif a été intégré par les mainteneurs mcollina et aduh95. Comme PHP2Shell la semaine précédente, la faille a été trouvée par analyse outillée, signe d'une recherche de vulnérabilités sur les composants d'infrastructure qui s'intensifie, et qui change le rythme auquel les défenseurs doivent réagir.
S'entraîner sur un lab réel
Lire une exploitation ne remplace pas de l'avoir faite. En attendant l'ouverture de la mission Node, le même principe « une faille du moteur, pas de l'application » se travaille déjà sur le parcours PHP → shell de Zeroday Cyber Academy : des cibles réelles déployées à la demande, exploitation pas à pas et remédiation avant/après, avec solveur de référence sur le capstone mémoire.
Sources
- Node.js, « Wednesday, October 21, 2026 Security Releases », avis officiel : https://nodejs.org/en/blog/vulnerability/october-2026-security-releases (2026-10-21)
- Vercel, « Node.js security advisory: Brotli decoder RCE (CVE-2026-111497) » : https://vercel.com/kb/bulletin/node-js-security-advisory (2026-10-21)
- Qualys, « Node.js Patches Brotli Remote Code Execution Flaw » : https://blog.qualys.com/product-tech/2026/10/23/node-js-patches-brotli-remote-code-execution-flaw-find-vulnerable-runtimes-before-attackers-do (2026-10-23)
- Cycode, « node2shell (CVE-2026-111497): What to Patch in Node.js » : https://cycode.com/blog/node2shell-cve-2026-111497/ (2026-10-23)
- MITRE, CWE-787 « Out-of-bounds Write » : https://cwe.mitre.org/data/definitions/787.html


