
Isoliertes Docker-Lab und nicht-destruktiver Python-Scanner zur Reproduktion von CVE-2026-94545, der Next.js next/og ImageResponse SVG-Injection, mit verwundbaren vs. gepatchten Kontrollen und PNG-Beweisen auf Pixelebene.
next/og ImageResponse SVG-Injection (GHSA-vcvr-r3jv-pc5j)Sicherheitsforschungsmaterial zum Reproduzieren und Validieren von CVE-2026-94545,
der Next.js ImageResponse SVG-Serialisierungs-Injection (Upstream: Satori
GHSA-wx4j-mvgx-mqwp), in einem isolierten, nur auf Loopback beschränkten Labor.
>=16.2.0 <16.3.6 (nur Node.js ImageResponse)26a52aff); 15.5.26 enthält
verwandte Härtung, 15.x ist nicht vom RCE-Problem betroffenNur auf Systemen verwenden, die Sie besitzen oder für deren Test Sie ausdrücklich autorisiert sind. Der Scanner ist nicht-destruktiv: Er rendert ein Bild und zählt Pixel. Auf dem Ziel wird nichts geschrieben, gespeichert oder ausgeführt.
Status: im mitgelieferten Labor bewiesen.
Gegen das exakte Routenmuster des Advisories (Angreiferwert in
<svg><title>{value}</title></svg>):
next 16.3.5 (verwundbar): injiziertes 500x150 rotes Rechteck in das PNG gerendert
(75.000 #FF0000 Pixel in der Antwort gemessen)
next 16.3.6 (gepatcht) : Wert escaped, 0 rote Pixel
satori 0.25.0 (verwundbar): Payload roh im serialisierten SVG-String vorhanden
satori 0.33.5 (behoben) : Payload zu </title> escaped
Dies beweist das Injection-Primitiv Ende-zu-Ende (Serializer -> eingebettetes SVG -> gerasterte Ausgabe). Es demonstriert keine Remote-Code-Ausführung: Das Advisory verknüpft RCE mit dem nachgelagerten SVG-Parser, dessen verwundbare Details zum Zeitpunkt des Schreibens nicht öffentlich sind. Es wird kein Dateilesen, SSRF oder Codeausführung beansprucht.
Satori serialisiert <svg>-Knoten mit translateSVGNodeToSVGString()
(src/handler/preprocess.ts), das ohne Escaping in die Ausgabe-XML
interpolierte:
String(node) zurückgegeben wurden - unescaped (jetzt escapeXMLText)." bricht aus.style-Werte roh interpoliert.& nicht, sodass innere XML-
Entities den äußeren SVG-Parse überlebten.expand.ts übergab interne Style-Eigenschaften (Präfix _) in serialisierte
Styles - jetzt abgelehnt.Der Fix (satori@26a52aff)
leitet alles durch ein härtendes buildXMLString(), das Werte escaped
und XML-Namen validiert. Dieses Labor verifiziert all dies verhaltensbasiert; die exakte
Anfrage und die Pixel-Beweise befinden sich in poc/.
Voraussetzungen: Docker mit Compose v2, Python 3.10+ auf dem Host (für den Scanner), Netzwerkzugriff auf npm beim ersten Lauf.
./lab verify
Baut die verwundbare App (gebündeltes next/og aus [email protected]), verlangt,
dass das Marker-Rechteck in der HTTP-Antwort gerendert wird, dann die gepatchte App
([email protected]) und verlangt dessen Abwesenheit.
Verwundbare Kontrolle:
[*] #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
Gepatchte Kontrolle:
[*] #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
Scanner manuell ausführen (Loopback-Standard; autorisierte externe Ziele benötigen die explizite Überschreibung):
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]
Die Payload ist balanciertes XML, das ein 500x150 reinrotes <rect> trägt. Auf einem
verwundbaren Endpunkt entkommt der Wert aus <title> und das Rechteck wird ein echtes
Element im eingebetteten SVG-Dokument; der Scanner zählt exakte #FF0000-
Pixel im zurückgegebenen PNG (75.000 erwartet; Schwelle 10.000). Auf einem gepatchten
Endpunkt ist der Wert escapter Text und es wird kein Rot gerendert. Die Probe ändert
nichts am Ziel: eine GET-Anfrage, ein Bild-Render.
Beachten Sie, dass der Träger absichtlich ein <rect> ist, kein Text: Text innerhalb des
eingebetteten SVG hat im Renderer keine ladbare Schriftart und wäre selbst bei
Injektion unsichtbar.
Der Beweis dieser Schwachstelle ist der Pixelinhalt eines gerasterten Bildes,
und nuclei-Matcher können PNGs nicht dekodieren. Ein Größen-Differenz-Template wurde
erstellt und verworfen: Bei einer gepatchten App wird die escapte Payload weiterhin gerendert (als
wörtlicher inerter Text), sodass sich die Größen von harmlos vs. Payload auch auf gepatchten Systemen
unterscheiden, und die Trennung zwischen verwundbaren und gepatchten Deltas
app-spezifisches Kompressionsrauschen ist. Einen Matcher auszuliefern, der auf gepatchten
Systemen anschlägt, ist schlimmer, als keinen auszuliefern. Verwenden Sie poc/scan.py; es wertet das
Antwortbild exakt aus.
| Version | Bewertung |
|---|---|
| Next.js 16.3.5 (gebündeltes @vercel/og) | Injection Ende-zu-Ende über HTTP reproduziert |
| Next.js 16.3.6 (gebündeltes @vercel/og) | Gepatchte Negativkontrolle reproduziert |
| satori 0.25.0 (npm) | Rohe Injection im serialisierten SVG (Quellcode-Ebene) |
| satori 0.33.5 (npm, 22. Sep. 2026) | Escaped (Quellcode-Ebene) |
| Next.js 16.2.0-16.3.4 | Betroffen gemäß Advisory; nicht einzeln getestet |
| Next.js 15.x | Nicht vom RCE-Problem betroffen gemäß Hersteller-Blog |
.
├── .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)
Nicht committet: assets/ (npm-gepackte next-Tarballs + extrahierte Bundles),
node_modules/, .lab-state.
| Behauptung | Status |
|---|---|
| SVG-Serialisierungs-Injection in satori < Fix | Bewiesen (Quellcode + Verhalten) |
| Ende-zu-Ende-Rendering von injiziertem Markup via next/og 16.3.5 | Bewiesen |
| Gepatchte Negativkontrolle (16.3.6 / satori 0.33.5) | Bewiesen |
| Attribut-/Style-Träger (gleiche Ursache) | Ursache im Fix-Diff bestätigt; Text-Träger bewiesen |
| SSRF / Dateilesen über injizierte Ressourcenreferenzen | Nicht getestet |
| Remote-Code-Ausführung | Nicht beansprucht (hängt von nicht offengelegten Details des nachgelagerten Parsers ab) |
Führen Sie dieses Repository nur auf Systemen aus, die Sie besitzen oder für deren Test Sie ausdrücklich
autorisiert sind. Der Labor-Treiber lehnt andere Varianten als vulnerable/patched ab, der
Scanner lehnt Nicht-Loopback-Ziele ab, es sei denn
--allow-authorized-non-loopback wird angegeben, und der Compose-Port ist an
127.0.0.1 gebunden.
Siehe SECURITY.md für den Umgang mit Offenlegungen. Dieses Repository enthält keine Produktionsziele, Kundendaten, Zugriffstoken oder Beweise aus realen Systemen.
MIT - siehe LICENSE.