
Laboratorio Docker isolato e scanner Python non distruttivo che riproduce CVE-2026-94545, l'iniezione SVG di Next.js next/og ImageResponse, con controlli vulnerabili vs corretti e prove PNG a livello di pixel.
next/og ImageResponse di Next.js (GHSA-vcvr-r3jv-pc5j)Materiale di ricerca sulla sicurezza per riprodurre e validare CVE-2026-94545,
l'iniezione nella serializzazione SVG di ImageResponse di Next.js (a monte: Satori
GHSA-wx4j-mvgx-mqwp), in un laboratorio isolato, solo su loopback.
>=16.2.0 <16.3.6 (solo ImageResponse di Node.js)26a52aff); 15.5.26 include
l'hardening correlato, 15.x non è interessato dal problema RCEUsare solo su sistemi di propria proprietà o per i quali si è esplicitamente autorizzati a eseguire test. Lo scanner è non distruttivo: renderizza un'immagine e conta i pixel. Nulla viene scritto, memorizzato o eseguito sul target.
Stato: dimostrato nel laboratorio fornito.
Contro il pattern di route esatto dell'advisory (valore dell'attaccante in
<svg><title>{value}</title></svg>):
next 16.3.5 (vulnerabile): rect rossa 500x150 iniettata renderizzata nel PNG
(75.000 pixel #FF0000 misurati nella risposta)
next 16.3.6 (corretto) : valore sottoposto a escaping, 0 pixel rossi
satori 0.25.0 (vuln) : payload presente in chiaro nella stringa SVG serializzata
satori 0.33.5 (corretto) : payload sottoposto a escaping in </title>
Questo dimostra la primitiva di iniezione end-to-end (serializzatore -> SVG incorporato -> output rasterizzato). Non dimostra l'esecuzione di codice remoto: l'advisory lega l'RCE al parser SVG a valle, i cui dettagli vulnerabili non sono pubblici al momento della stesura. Non viene rivendicata alcuna lettura di file, SSRF o esecuzione di codice.
Satori serializza i nodi <svg> con translateSVGNodeToSVGString()
(src/handler/preprocess.ts), che interpolava nell'output XML senza
escaping:
String(node) - senza escaping (ora escapeXMLText)." permette di uscire.style interpolati in chiaro.&, quindi le entità XML
interne sopravvivevano al parsing dell'SVG esterno.expand.ts passava proprietà di stile interne (prefisso _) negli stili
serializzati - ora rifiutate.La correzione (satori@26a52aff)
instrada tutto attraverso un buildXMLString() di hardening che esegue l'escaping dei valori
e valida i nomi XML. Questo laboratorio verifica tutto ciò a livello comportamentale; la richiesta
esatta e le prove sui pixel sono in poc/.
Requisiti: Docker con Compose v2, Python 3.10+ sull'host (per lo scanner), accesso di rete a npm alla prima esecuzione.
./lab verify
Compila l'app vulnerabile (next/og fornito da [email protected]), richiede
che la rect marcatore venga renderizzata nella risposta HTTP, poi l'app corretta
([email protected]) e richiede la sua assenza.
Controllo vulnerabile:
[*] #FF0000 pixels: 75000 (threshold 10000)
[!!!] INJECTION CONFIRMED: the attacker value escaped <title> and
became a rendered SVG element in the generated image.
RESULT: VULNERABLE CONTROL CONFIRMED
Controllo corretto:
[*] #FF0000 pixels: 0 (threshold 10000)
[=] no injection observed
RESULT: PATCHED CONTROL CONFIRMED
./lab start vulnerable # 16.3.5 app on http://127.0.0.1:9481/og
./lab test # assert injection renders
./lab start patched # 16.3.6 app
./lab test # assert absence
./lab status
./lab reset
Eseguire lo scanner manualmente (loopback predefinito; i target esterni autorizzati richiedono l'override esplicito):
python3 poc/scan.py http://127.0.0.1:9481/og
python3 poc/scan.py https://host.example/og --param value \
--allow-authorized-non-loopback [--insecure]
Il payload è XML bilanciato che trasporta un <rect> 500x150 rosso puro. Su un
endpoint vulnerabile il valore esce da <title> e la rect diventa un vero
elemento nel documento SVG incorporato; lo scanner conta i pixel esatti #FF0000
nel PNG restituito (75.000 attesi; soglia 10.000). Su un endpoint corretto
il valore è testo sottoposto a escaping e nessun rosso viene renderizzato. La sonda non modifica
nulla sul target: una richiesta GET, un rendering di immagine.
Si noti che il vettore è deliberatamente un <rect>, non testo: il testo all'interno
dell'SVG incorporato non ha un font caricabile nel renderer e sarebbe invisibile
anche quando iniettato.
La prova di questa vulnerabilità è il contenuto in pixel di un raster renderizzato,
e i matcher di nuclei non possono decodificare i PNG. È stato creato e rifiutato un template
basato sul differenziale di dimensione: su un'app corretta il payload sottoposto a escaping viene comunque renderizzato (come
testo letterale inerte), quindi le dimensioni benigno-vs-payload differiscono anche sui sistemi corretti,
e la separazione tra i delta vulnerabile e corretto è
rumore di compressione specifico dell'app. Distribuire un matcher che scatta su sistemi corretti
è peggio che non distribuirne nessuno. Usare poc/scan.py; valuta l'immagine
di risposta esattamente.
| Versione | Valutazione |
|---|---|
| Next.js 16.3.5 (@vercel/og fornito) | Iniezione riprodotta end-to-end su HTTP |
| Next.js 16.3.6 (@vercel/og fornito) | Controllo negativo corretto riprodotto |
| satori 0.25.0 (npm) | Iniezione in chiaro nell'SVG serializzato (a livello di sorgente) |
| satori 0.33.5 (npm, 22 set 2026) | Sottoposto a escaping (a livello di sorgente) |
| Next.js 16.2.0-16.3.4 | Interessato secondo l'advisory; non testato singolarmente |
| Next.js 15.x | Non interessato dal problema RCE secondo il blog del fornitore |
.
├── .gitignore
├── LICENSE
├── README.md
├── SECURITY.md
├── docker-compose.yml
├── lab
├── app/
│ └── server.mjs # advisory route pattern over loopback HTTP
└── poc/
└── scan.py # non-destructive scanner (Python stdlib: urllib + zlib)
Non committati: assets/ (tarball next impacchettati da npm + bundle estratti),
node_modules/, .lab-state.
| Affermazione | Stato |
|---|---|
| Iniezione nella serializzazione SVG in satori < fix | Dimostrata (sorgente + comportamento) |
| Rendering end-to-end del markup iniettato tramite next/og 16.3.5 | Dimostrato |
| Controllo negativo corretto (16.3.6 / satori 0.33.5) | Dimostrato |
| Vettori attributo/style (stessa causa principale) | Causa principale confermata nel diff di correzione; vettore di testo dimostrato |
| SSRF / lettura di file tramite riferimenti a risorse iniettati | Non testato |
| Esecuzione di codice remoto | Non rivendicata (dipende da dettagli non divulgati del parser a valle) |
Eseguire questo repository solo su sistemi di propria proprietà o per i quali si è esplicitamente autorizzati a
eseguire test. Il driver del laboratorio rifiuta varianti diverse da vulnerable/patched, lo
scanner rifiuta target non loopback a meno che non venga fornito
--allow-authorized-non-loopback, e la porta di Compose è associata
a 127.0.0.1.
Vedere SECURITY.md per la gestione della divulgazione. Questo repository non include target di produzione, dati dei clienti, token di accesso o prove da sistemi reali.
MIT - vedere LICENSE.