
Lab Docker isolé et scanner Python non destructif reproduisant CVE-2026-94545, l'injection SVG de Next.js next/og ImageResponse, avec contrôles vulnérable vs corrigé et preuves PNG au niveau du pixel.
next/og ImageResponse de Next.js (GHSA-vcvr-r3jv-pc5j)Matériel de recherche de sécurité pour reproduire et valider CVE-2026-94545,
l'injection de sérialisation SVG dans ImageResponse de Next.js (amont : Satori
GHSA-wx4j-mvgx-mqwp), dans un laboratoire isolé, en boucle locale uniquement.
>=16.2.0 <16.3.6 (Node.js ImageResponse uniquement)26a52aff) ; 15.5.26 embarque
un durcissement associé, 15.x n'est pas affecté par le problème de RCEÀ utiliser uniquement sur des systèmes que vous possédez ou que vous êtes explicitement autorisé à tester. Le scanner est non destructif : il rend une image et compte les pixels. Rien n'est écrit, stocké ou exécuté sur la cible.
Statut : prouvé dans le laboratoire fourni.
Contre le modèle de route exact de l'avis (valeur de l'attaquant dans
<svg><title>{value}</title></svg>) :
next 16.3.5 (vulnérable) : rect rouge 500x150 injecté rendu dans le PNG
(75 000 pixels #FF0000 mesurés dans la réponse)
next 16.3.6 (corrigé) : valeur échappée, 0 pixel rouge
satori 0.25.0 (vuln) : payload présent brut dans la chaîne SVG sérialisée
satori 0.33.5 (corrigé) : payload échappé en </title>
Cela prouve la primitive d'injection de bout en bout (sérialiseur -> SVG embarqué -> sortie rastérisée). Cela ne démontre pas l'exécution de code à distance : l'avis lie la RCE au parseur SVG en aval, dont les détails vulnérables ne sont pas publics au moment de la rédaction. Aucune lecture de fichier, SSRF ou exécution de code n'est revendiquée.
Satori sérialise les nœuds <svg> avec translateSVGNodeToSVGString()
(src/handler/preprocess.ts), qui interpolait dans la sortie XML sans
échappement :
String(node) - non échappés (désormais escapeXMLText)." permet de sortir.style interpolées brutes.&, donc les entités XML
internes survivaient à l'analyse du SVG externe.expand.ts transmettait les propriétés de style internes (préfixe _) dans les
styles sérialisés - désormais rejetées.La correction (satori@26a52aff)
fait tout passer par un buildXMLString() de durcissement qui échappe les valeurs
et valide les noms XML. Ce laboratoire vérifie tout cela de manière comportementale ; la requête
exacte et les preuves de pixels sont dans poc/.
Prérequis : Docker avec Compose v2, Python 3.10+ sur l'hôte (pour le scanner), accès réseau à npm au premier lancement.
./lab verify
Construit l'application vulnérable (next/og vendu depuis [email protected]), exige
que le rect marqueur soit rendu dans la réponse HTTP, puis l'application corrigée
([email protected]) et exige son absence.
Contrôle vulnérable :
[*] #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
Contrôle corrigé :
[*] #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
Exécuter le scanner à la main (boucle locale par défaut ; les cibles externes autorisées nécessitent l'option explicite) :
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]
Le payload est du XML équilibré portant un <rect> rouge pur de 500x150. Sur un
point de terminaison vulnérable, la valeur sort de <title> et le rect devient un véritable
élément dans le document SVG embarqué ; le scanner compte les pixels #FF0000 exacts
dans le PNG renvoyé (75 000 attendus ; seuil 10 000). Sur un point de terminaison corrigé,
la valeur est du texte échappé et aucun rouge n'est rendu. La sonde ne change
rien sur la cible : une requête GET, un rendu d'image.
Notez que le porteur est délibérément un <rect>, pas du texte : le texte à l'intérieur du
SVG embarqué n'a pas de police chargeable dans le moteur de rendu et serait invisible
même lorsqu'il est injecté.
La preuve de cette vulnérabilité est le contenu en pixels d'un raster rendu,
et les matchers nuclei ne peuvent pas décoder les PNG. Un template de différentiel de taille a été
construit et rejeté : sur une application corrigée, le payload échappé est toujours rendu (comme
du texte littéral inerte), donc les tailles bénin-vs-payload diffèrent aussi sur les systèmes corrigés,
et la séparation entre les deltas vulnérables et corrigés est du bruit de compression
spécifique à l'application. Livrer un matcher qui se déclenche sur des systèmes corrigés est pire que
de n'en livrer aucun. Utilisez poc/scan.py ; il évalue l'image de réponse exactement.
| Version | Évaluation |
|---|---|
| Next.js 16.3.5 (@vercel/og vendu) | Injection reproduite de bout en bout via HTTP |
| Next.js 16.3.6 (@vercel/og vendu) | Contrôle négatif corrigé reproduit |
| satori 0.25.0 (npm) | Injection brute dans le SVG sérialisé (niveau source) |
| satori 0.33.5 (npm, 22 Sep 2026) | Échappé (niveau source) |
| Next.js 16.2.0-16.3.4 | Affecté selon l'avis ; non testé individuellement |
| Next.js 15.x | Non affecté par le problème de RCE selon le blog du fournisseur |
.
├── .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 commité : assets/ (tarballs next empaquetés par npm + bundles extraits),
node_modules/, .lab-state.
| Affirmation | Statut |
|---|---|
| Injection de sérialisation SVG dans satori < correction | Prouvé (source + comportement) |
| Rendu de bout en bout du balisage injecté via next/og 16.3.5 | Prouvé |
| Contrôle négatif corrigé (16.3.6 / satori 0.33.5) | Prouvé |
| Porteurs d'attributs/styles (même cause racine) | Cause racine confirmée dans le diff de correction ; porteur texte prouvé |
| SSRF / lecture de fichier via des références de ressources injectées | Non testé |
| Exécution de code à distance | Non revendiquée (dépend de détails non divulgués du parseur en aval) |
N'exécutez ce dépôt que sur des systèmes que vous possédez ou que vous êtes explicitement autorisé à
tester. Le pilote du laboratoire refuse les variantes autres que vulnerable/patched, le
scanner refuse les cibles non-loopback sauf si
--allow-authorized-non-loopback est fourni, et le port Compose est lié
à 127.0.0.1.
Voir SECURITY.md pour la gestion de la divulgation. Ce dépôt ne contient pas de cibles de production, de données clients, de jetons d'accès ou de preuves provenant de systèmes réels.
MIT - voir LICENSE.