Node2Shell: la falla di Node.js e chi deve aggiornare il runtime

Server con il logo esagonale di Node.js in una sala macchine synthwave, attraversato da un flusso di dati compressi rossi, mentre un carrello con una cassa azzurra resta fermo in corridoio
La patch esiste dal 21 ottobre. Resta da capire chi deve installarla.
data: · 9 min
autore: Matteo

Node2Shell è il nome dato alla CVE-2026-111497, una falla nel decoder Brotli che Node.js si porta dentro. Dopo PHP2Shell sembra di rivedere lo stesso film con un altro linguaggio: il bug non sta nel tuo codice e nemmeno nei tuoi pacchetti, sta nel runtime.

Appena l’ho letta ho pensato al vibe coding. Se la tua app l’ha generata Lovable, sai che versione di Node la fa girare? Sai almeno se c’è, Node? Sono andato a controllare e la risposta è più interessante di quella che mi aspettavo. Per un’app pubblicata su Lovable il problema sembra già gestito da altri. Per la stessa app esportata su un tuo server, il problema è tuo.

Nessuna delle analisi tecniche che ho letto collega Node2Shell a Lovable o simili, quindi il ragionamento su queste piattaforme è mio. Ti segnalo dove finiscono i fatti e dove comincia la mia opinione.

Cos’è Node2Shell (spiegato senza puntatori)

Brotli è un algoritmo di compressione di Google, quello che spesso comprime le pagine web che ricevi. Il suo decoder, dalla versione 1.0.3 alla 1.2.0, gestisce male un caso preciso: un cambio di blocco non valido viene scambiato per una semplice richiesta di altri dati, e il programma finisce per scrivere oltre la fine di un buffer di 8 byte.

È come un postino che, davanti a un indirizzo sbagliato, invece di fermarsi infila la lettera nella cassetta del vicino. Con la lettera giusta e il vicino giusto, a qualcuno può andare molto male.

Node.js include questa libreria, ma la stessa falla tocca anche Bun, .NET, i pacchetti Python Brotli e brotlicffi, Envoy e Istio. Per questo la trovi anche con altri nomi: HTTP2Shell, Broccoli2Shell, Brotli2Shell.

Due dettagli raccontano bene la storia:

  • L’ha trovata un’IA. Il bollettino di Node.js accredita OpenAI Codex Security. L’IA che scrive il codice e l’IA che ci trova i buchi, ormai, lavorano nello stesso palazzo.
  • La correzione è stata in chiaro per otto mesi. Secondo Help Net Security il commit in google/brotli è del 27 febbraio, senza release né avviso. Il messaggio la definiva pure di bassa gravità.

Chi è esposto a Node2Shell

Node.js non decomprime Brotli di sua iniziativa: serve del codice, tuo o di una libreria, che decomprima dati controllati da chi attacca. Il guaio è che quel codice è spesso già lì, senza che tu l’abbia chiesto.

ScenarioEsposto?
Express 5 con express.json()sì, di default
Express 4no, risponde 415
Server HTTP Node senza decompressioneno
Koa, NestJS, Strapi, Eggsì
Fastifycon @fastify/compress
fetch(), axios, got lato server verso URL scelti dall’utentesì, decodificano la risposta in automatico
Next.js self-hostedsì, tramite l’ottimizzazione delle immagini

La riga del fetch() è quella che mi preoccupa di più: anteprime dei link, webhook, crawler, agenti IA che leggono una pagina. Il server va a prendere un URL deciso da altri, e chi controlla quell’URL decide anche cosa torna indietro. Sono funzioni che oggi si aggiungono con una riga di prompt.

Ho voluto vedere la riga di Express con i miei occhi. Ho installato Express con un semplice npm i express (arriva la 5.3.0), ho scritto tre righe di express.json() e ho spedito un corpo JSON compresso con Brotli, innocuo. Risposta: 200, JSON letto correttamente. Nessuna configurazione, nessuna scelta da parte mia.

Se vuoi fare la stessa prova su una tua app in locale (è un dato innocuo, serve solo a capire se il parser decomprime):

node -e "process.stdout.write(require('zlib').brotliCompressSync('{\"hello\":\"brotli\"}'))" > body.br

curl -i -X POST http://localhost:3000/tua-rotta \
  -H 'Content-Type: application/json' \
  -H 'Content-Encoding: br' \
  --data-binary @body.br

Se la rotta risponde come a un JSON normale, il decoder viene raggiunto. Se ricevi 415, da quel percorso no. Vale solo per quella rotta, e non dice nulla sul fetch().

Quanto è grave davvero: gli esperti non sono d’accordo

Qui bisogna andarci piano, perché le fonti si contraddicono:

  • Gravità: 9,2 critica secondo il record CVE, “high” per Node.js.
  • Sysdig scrive che l’esecuzione di codice affidabile è stata riprodotta in un solo ambiente (Imperva, immagine ufficiale 24.22.0 su arm64), mentre far cadere il processo è facile. Altri ricercatori sostengono che l’impatto reale sia il blocco del servizio.
  • ELLIO riporta una shell su quattro bersagli x86-64 con la stessa 24.22.0, Express 5.3.0 e Koa 3 compresi. Però ogni build richiede offset propri: gli stessi valori su 22.23.3 e 26.12.0 non hanno prodotto nulla in 10.000 tentativi, e la maggior parte dei 400 tentativi contro Express ha solo ucciso il worker.
  • Datadog ha ottenuto una lettura ripetibile della memoria e un crash deterministico. Niente shell, ma giudica il resto una questione di tempo e lavoro.

Sul fronte attacchi, CISA l’ha aggiunta al catalogo KEV il 27 ottobre, con scadenza 10 novembre per le agenzie federali americane, senza dare dettagli. I casi pubblici che ho trovato sono tentativi sui honeypot: quello documentato da ELLIO ha fatto zero centri su 50 contro un bersaglio pulito. Curiosità: l’IP di provenienza era già stato collegato a PHP2Shell. Vittime confermate, per ora, non ne ho trovate.

Riassumendo: chi ha una versione vecchia rischia di sicuro il blocco del servizio, e l’esecuzione di codice è dimostrata ma non banale. Non è un buon motivo per rimandare: i costi di un aggiornamento sono minuti, e gli exploit tendono a migliorare, non a peggiorare.

Chi aggiorna Node? Lovable, Replit e il vibe coding

Questa è la domanda che mi interessa, perché npm update qui non serve a niente. Il bug è compilato dentro il runtime. Quindi: chi lo cambia?

Dove gira l’appChi aggiornaCosa risulta
Lovable, app pubblicataLovable e CloudflareOgni app è un worker per workerd. Un collaboratore del progetto ha scritto il 27 ottobre che è “patched in production”. Da Lovable nessun avviso trovato
Funzioni su SupabaseSupabaseGirano su Deno e il manifest di Edge Runtime dichiara il crate Rust brotli, non il decoder C di Google. È una mia lettura del codice, non l’ho testata e non c’è una dichiarazione ufficiale
VercelVercel, ma tu devi ridistribuireDal 28 ottobre le nuove distribuzioni sono corrette. Le esistenti vanno ridistribuite, e su Node 20 devi cambiare versione
Replitda verificareGuide di terzi descrivono app Node avviate con node dist/index.js. Nessun avviso trovato
Lovable esportata su un tuo servertuVedi sotto

Che sul tuo server gli aggiornamenti tocchino a te è ovvio, e Lovable lo scrive anche: la guida per ospitare le app altrove dice che su un’infrastruttura tua aggiornamenti e sicurezza sono a carico tuo. Meno ovvio è cosa ci sia da aggiornare. Le app Lovable create dal 13 maggio 2026 usano TanStack Start, quindi hanno codice che gira sul server, e la stessa guida dice di usare “Node.js 22”, senza indicare una versione minima.

Da qui in poi è opinione mia. Il vibe coding non ha creato questa falla, e un’app su una piattaforma gestita sta probabilmente meglio di tante applicazioni scritte a mano e dimenticate su una VPS dal 2023. Il rischio nasce nel passaggio: il giorno in cui esporti il progetto per risparmiare sull’abbonamento, ti ritrovi amministratore di sistema. L’avviso c’è, ma sapere che tocca a te non ti dice cosa controllare. Prima avevi un servizio. Ora hai un Dockerfile scritto da un modello, una versione di Node che non hai scelto e nessuna notifica quando esce una patch.

Non ho numeri su quante app siano in questa situazione, e non credo ce li abbia nessuno. So però come ero messo io.

Ho controllato il mio Mac: tutto da aggiornare

Prima di dare lezioni ho lanciato qualche node --version. Sono ambienti di sviluppo, non server pubblici, ma il risultato è istruttivo:

DoveVersioneStato
Node installato sul Mac26.0.0vecchia
Container con Strapi22.23.3vecchia
Container di un frontend22.22.2vecchia
Container di una dashboard22.19.0vecchia
Immagine node:22-bookworm-slim22.23.3vecchia
Immagine n8nio/n8n:latest22.22.0vecchia, e costruita 8 mesi fa

Zero su sei. Lo Strapi, provato con lo stesso corpo Brotli innocuo, ha risposto come a un JSON normale, senza nessun 415. E notare quel latest, che sul disco significa “l’ultima che ho scaricato”, ovvero otto mesi fa. Ecco, io questo mestiere lo faccio da un po’ e ci sono cascato lo stesso. Il problema non è di chi non sa programmare: Node è il pezzo che nessuno guarda, perché di solito funziona.

Come correggere Node2Shell

1. Le versioni corrette

RamoVersione con la correzione
Node.js 2626.12.1
Node.js 2424.22.1
Node.js 2222.23.4
Node.js 20, 23, 25nessuna correzione, fine vita: va cambiato ramo
Bun1.4.4

Se usi il Node di una distribuzione collegato al Brotli di sistema, il numero di versione da solo non basta: controlla l’avviso del distributore.

2. Controllare la versione che gira davvero

# Sul server o in locale
node --version

# Dentro un container in esecuzione
docker exec NOME_CONTAINER node --version

# Dentro un'immagine (scarica prima quella aggiornata)
docker pull node:24-slim
docker run --rm node:24-slim node --version

Nel dubbio, fai scrivere process.version ai log all’avvio: è l’unica risposta che riguarda il processo vero.

3. Aggiornare e ricostruire

Riavviare un container non cambia il suo Node. L’immagine va ricostruita:

docker compose build --pull
docker compose up -d

Se il Dockerfile dice FROM node:24.22.0, resterà vulnerabile per quante volte lo ricostruisci: la versione va alzata a mano. Vale anche per il Dockerfile che ti ha scritto il modello, e vale per i pacchetti globali e le immagini di terzi che si portano dentro il proprio Node.

Su una piattaforma gestita, cerca il suo avviso e, nel dubbio, ridistribuisci. Poi verifica: fidarsi va bene, controllare è meglio.

4. Se proprio non puoi aggiornare subito

Sono misure tampone, che non sostituiscono la patch:

  • express.json({ inflate: false }): l’ho provato e risponde 415, ma blocca anche il gzip;
  • rifiuta Content-Encoding: br sul proxy davanti all’app, se nessun client legittimo lo usa;
  • cerca nel codice fetch, createBrotliDecompress e simili dove l’URL arriva dall’utente;
  • esegui il servizio con un utente senza privilegi e tieni i segreti fuori dalle variabili d’ambiente, per limitare i danni.

Occhio ai WAF: Vercel precisa che le sue regole non coprono le risposte scaricate dal tuo codice, e quelle di emergenza di Cloudflare partono in sola registrazione. Nei log cerca worker terminati con segnale 139 subito dopo una richiesta Brotli, e soprattutto shell o connessioni in uscita partite dal processo web.

In sintesi

  • Node2Shell (CVE-2026-111497) è una scrittura fuori dai limiti nel decoder Brotli, presente in Node.js e in molto altro software.
  • Colpisce chi decomprime dati controllati da altri: Express 5 lo fa di default, e anche il fetch() lato server verso URL scelti dall’utente è esposto.
  • È nel catalogo CISA. Il crash è facile, l’esecuzione di codice è dimostrata ma non banale, e non ho trovato vittime confermate.
  • La soluzione è Node 22.23.4, 24.22.1 o 26.12.1, ricostruendo le immagini. npm update non basta.
  • Non è un problema del vibe coding in sé: su una piattaforma gestita la patch la mette qualcun altro. Quando esporti l’app, quel qualcuno sei tu.

Una cosa la puoi fare adesso: lancia node --version dove gira la tua app. Se non sai dove lanciarlo, hai già trovato il primo problema.

Fonti: bollettino di Node.js, Sysdig, ELLIO, Datadog Security Labs, SecurityWeek, Help Net Security, Vercel, issue su workerd, documentazione di Lovable.

Copiato