OWASP & AppSec

Node2Shell (CVE-2026-111497) : la RCE Brotli de Node.js

Node2Shell (CVE-2026-111497) : écriture hors limites du décodeur Brotli de Node.js menant à une RCE. Mécanisme, exposition réelle via fetch, correctif.

Naïm Aouaichia
13 min de lecture
  • Node.js
  • RCE
  • CVE-2026-111497
  • Brotli
  • Corruption mémoire
  • OWASP

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 . | head

3. 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.jsStatutVersion corrigée
26.xAffectée26.12.1
24.xAffectée24.22.1
22.x (LTS)Affectée22.23.4
20.x et antérieuresHors support, pas de correctifMigrer vers 22.x ou 24.x
BunAffecté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-slim

2. 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 :

CVESévéritéEn une ligne
CVE-2026-111497highOOB write du décodeur Brotli, menant à une RCE (Node2Shell)
CVE-2026-105483highContournement de hostname TLS via des chiffres en pleine chasse
CVE-2026-106069highContournement de hostname TLS via résolution IP permissive
CVE-2026-105482highTroncature de noms contenant un octet NUL dans dns.resolve
CVE-2026-105481mediumSocket keep-alive traitant une réponse écrite avant la requête
CVE-2026-106072mediumValidation de certificat laxiste sur hostnames IPv4 non canoniques
CVE-2026-106074mediumCaches d'entropie de randomUUID/randomInt figés dans les snapshots
CVE-2026-106070lowZipFile renvoyant des octets contrôlés après échec de validation (26.x)
CVE-2026-106071lowSur-autorisation du modèle de permissions par règle joker (26.x)
CVE-2026-106073lowContournement 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, fetch sortant compris, pour mesurer l'exposition réelle plutôt que la supposer.
  • Ne pas confondre le paquet npm brotli avec le décodeur natif de node: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

Questions fréquentes

  • Qu'est-ce que Node2Shell exactement ?
    Node2Shell, aussi appelé Brotli2Shell, est le nom informel donné à CVE-2026-111497, une écriture hors limites dans le décodeur Brotli embarqué dans Node.js. L'avis officiel Node.js du 21 octobre 2026 la classe « high ». Comme PHP2Shell une semaine plus tôt, c'est une faille du moteur, pas de l'application : le défaut est dans le runtime, donc une application au code irréprochable reste vulnérable tant que Node.js n'est pas à jour. L'enjeu est le même, systémique : Node.js fait tourner une large part des services web modernes, et une faille de son décodeur de compression touche en même temps tout service qui décompresse une donnée Brotli venue de l'extérieur.
  • Mon application est-elle concernée si je n'utilise pas Brotli directement ?
    Peut-être, et c'est le point le plus mal compris. L'exposition ne dépend pas d'un appel explicite à Brotli dans votre code : elle dépend du fait que votre application décompresse une donnée Brotli que l'attaquant contrôle. Cela inclut les réponses récupérées par fetch() avec un en-tête Content-Encoding: br, et toute entrée qui transite par node:zlib en mode Brotli. Seule la mise à jour du runtime couvre le cas des réponses fetch. 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.
  • Quelles versions corrigent CVE-2026-111497 ?
    Les correctifs sont livrés dans Node.js 22.23.4, 24.22.1 et 26.12.1, publiés le ou peu après le 21 octobre 2026. La faille affecte les branches 22.x, 24.x et 26.x. Les branches plus anciennes, dont la 20.x, n'ont pas de correctif officiel dans cet avis : un service qui y est resté doit migrer vers une branche maintenue, pas seulement appliquer un patch. Bun a intégré le correctif en 1.4.4. Le même runtime côté PHP a connu sa propre faille mémoire la même semaine, voir PHP2Shell.
  • Quel score de gravité faut-il retenir ?
    Node.js lui-même ne publie pas de score CVSS chiffré : l'avis officiel qualifie la faille de « high », et précise que la plus haute sévérité du lot d'octobre 2026 est HIGH. Le score de 9,2 sur CVSS 4.0 qui circule provient du CNA Google, pas de Node.js. L'écart n'est pas anodin : une écriture hors limites menant à une RCE mérite une priorité haute, mais présenter un chiffre de tiers comme s'il venait de Node.js serait une erreur de rigueur. Au 26 octobre 2026, aucune exploitation dans la nature n'a été rapportée et la faille n'est pas au catalogue KEV de la CISA.
  • Pourquoi valider la donnée décompressée et pas les octets reçus ?
    Parce que la faille se déclenche PENDANT la décompression, avant que votre validation applicative ne voie quoi que ce soit. Un pare-feu ou un filtre placé sur les octets Brotli bruts est aveugle : il inspecte un flux compressé dont il ne connaît pas le contenu, et le décodeur est atteint avant lui. C'est la leçon défensive de Node2Shell, et elle vaut au-delà de ce CVE : la frontière de confiance est le point où une donnée est interprétée, pas le point où elle entre. Tant que le décodeur n'est pas corrigé, aucune règle en amont ne protège. Le vrai correctif est la mise à jour du runtime.
  • Comment vérifier rapidement si je suis vulnérable ?
    Trois vérifications, dans l'ordre. D'abord la version du runtime : node --version, puis comparez à 22.23.4, 24.22.1 et 26.12.1 (toute version inférieure sur ces branches, ou une branche 20.x et antérieure, est exposée). Ensuite l'usage : cherchez dans votre code et vos dépendances les appels à node:zlib en mode Brotli (brotliDecompress, createBrotliDecompress, constante BROTLI_DECODE) et les fetch dont la réponse peut arriver en Content-Encoding: br. Enfin la surface : toute route qui accepte un corps compressé Brotli d'un client, ou qui appelle une API tierce et décompresse sa réponse. Une seule de ces trois cases suffit à justifier la mise à jour immédiate.
  • Les autres failles du lot d'octobre 2026 sont-elles graves ?
    Le même avis Node.js du 21 octobre 2026 corrige neuf CVE au total. Outre Node2Shell, deux contournements de vérification de hostname TLS classés « high » (CVE-2026-105483 et CVE-2026-106069) méritent attention : ils permettent à un certificat de passer pour un autre hôte via des chiffres en pleine chasse ou une résolution IP permissive, ce qui ouvre la voie à des interceptions. S'y ajoutent une troncature DNS sur octet NUL, et plusieurs failles du modèle de permissions sur la branche 26.x. La mise à jour du runtime les corrige toutes en même temps : c'est un argument de plus pour ne pas se contenter de « patcher Brotli ».

Trouver ces failles dans votre code, puis les refermer.

Nous reprenons les catégories OWASP sur des applications qui tournent : on exploite, on mesure ce que ça donne, puis on corrige et on vérifie que le correctif tient.

Écrit par

Naïm Aouaichia

Fondateur et directeur pédagogique de Zeroday Cyber Academy

Ingénieur cybersécurité avec un parcours hybride : développement, DevOps en ESN française, DevSecOps IN Groupe (sécurité des documents d'identité régaliens), audits CAC 40. Fondateur de Hash24Security et Zeroday Cyber Academy.