
Laboratório Docker isolado e scanner Python não destrutivo que reproduz o CVE-2026-94545, a injeção de SVG no ImageResponse do next/og do Next.js, com controles vulnerável vs corrigido e evidência PNG em nível de pixel.
next/og ImageResponse do Next.js (GHSA-vcvr-r3jv-pc5j)Material de pesquisa de segurança para reproduzir e validar o CVE-2026-94545,
a injeção de serialização de SVG no ImageResponse do Next.js (upstream: Satori
GHSA-wx4j-mvgx-mqwp), num laboratório isolado, apenas em loopback.
>=16.2.0 <16.3.6 (apenas ImageResponse do Node.js)26a52aff); 15.5.26 inclui
reforço relacionado, 15.x não é afetado pelo problema de RCEUse apenas em sistemas que possui ou que está explicitamente autorizado a testar. O scanner é não destrutivo: renderiza uma imagem e conta pixels. Nada é escrito, armazenado ou executado no alvo.
Estado: comprovado no laboratório fornecido.
Contra o padrão exato de rota do advisory (valor do atacante em
<svg><title>{value}</title></svg>):
next 16.3.5 (vulnerável): rect vermelho 500x150 injetado renderizado no PNG
(75.000 pixels #FF0000 medidos na resposta)
next 16.3.6 (corrigido) : valor escapado, 0 pixels vermelhos
satori 0.25.0 (vuln) : payload presente em bruto na string SVG serializada
satori 0.33.5 (corrigido): payload escapado para </title>
Isto prova a primitiva de injeção de ponta a ponta (serializador -> SVG incorporado -> saída rasterizada). Não demonstra execução remota de código: o advisory associa a RCE ao parser de SVG a jusante, cujos detalhes vulneráveis não são públicos no momento da escrita. Não é reivindicada leitura de ficheiros, SSRF ou execução de código.
O Satori serializa nós <svg> com translateSVGNodeToSVGString()
(src/handler/preprocess.ts), que interpolava na saída XML sem
escape:
String(node) - sem escape (agora escapeXMLText)." quebra o contexto.style interpolados em bruto.&, pelo que entidades XML internas
sobreviviam ao parse do SVG externo.expand.ts passava propriedades de estilo internas (prefixo _) para estilos
serializados - agora rejeitadas.A correção (satori@26a52aff)
encaminha tudo através de um buildXMLString() de reforço que escapa valores
e valida nomes XML. Este laboratório verifica tudo isso comportamentalmente; o pedido exato e a evidência de pixels estão em poc/.
Requisitos: Docker com Compose v2, Python 3.10+ no host (para o scanner), acesso de rede ao npm na primeira execução.
./lab verify
Constrói a aplicação vulnerável (next/og vendorizado de [email protected]), exige
que o rect marcador seja renderizado na resposta HTTP, depois a aplicação corrigida
([email protected]) e exige a sua ausência.
Controlo vulnerável:
[*] #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
Controlo corrigido:
[*] #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
Execute o scanner manualmente (loopback por omissão; alvos externos autorizados precisam da substituição explícita):
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]
O payload é XML equilibrado transportando um <rect> 500x150 vermelho puro. Num
endpoint vulnerável o valor escapa de <title> e o rect torna-se um elemento real no documento SVG incorporado; o scanner conta pixels exatos #FF0000
no PNG devolvido (75.000 esperados; limiar 10.000). Num endpoint corrigido
o valor é texto escapado e nenhum vermelho é renderizado. A sonda não altera
nada no alvo: um pedido GET, uma renderização de imagem.
Note que o portador é deliberadamente um <rect>, não texto: texto dentro do
SVG incorporado não tem fonte carregável no renderizador e seria invisível
mesmo quando injetado.
A prova desta vulnerabilidade é o conteúdo de pixels de um raster renderizado,
e os matchers do nuclei não conseguem descodificar PNGs. Um template de diferencial de tamanho foi
construído e rejeitado: numa aplicação corrigida o payload escapado ainda é renderizado (como
texto literal inerte), pelo que os tamanhos benigno-vs-payload diferem também em sistemas corrigidos,
e a separação entre deltas vulneráveis e corrigidos é ruído de compressão específico da aplicação. Enviar um matcher que dispara em sistemas
corrigidos é pior do que não enviar nenhum. Use poc/scan.py; avalia a
imagem de resposta exatamente.
| Versão | Avaliação |
|---|---|
| Next.js 16.3.5 (@vercel/og vendorizado) | Injeção reproduzida de ponta a ponta sobre HTTP |
| Next.js 16.3.6 (@vercel/og vendorizado) | Controlo negativo corrigido reproduzido |
| satori 0.25.0 (npm) | Injeção em bruto no SVG serializado (nível de código) |
| satori 0.33.5 (npm, 22 Set 2026) | Escapado (nível de código) |
| Next.js 16.2.0-16.3.4 | Afetado segundo o advisory; não testado individualmente |
| Next.js 15.x | Não afetado pelo problema de RCE segundo o blog do fornecedor |
.
├── .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)
Não versionado: assets/ (tarballs next empacotados pelo npm + bundles extraídos),
node_modules/, .lab-state.
| Afirmação | Estado |
|---|---|
| Injeção de serialização de SVG no satori < correção | Provado (código + comportamento) |
| Renderização de ponta a ponta de markup injetado via next/og 16.3.5 | Provado |
| Controlo negativo corrigido (16.3.6 / satori 0.33.5) | Provado |
| Portadores de atributo/estilo (mesma causa raiz) | Causa raiz confirmada no diff de correção; portador de texto provado |
| SSRF / leitura de ficheiros via referências de recursos injetadas | Não testado |
| Execução remota de código | Não reivindicado (depende de detalhes não divulgados do parser a jusante) |
Execute este repositório apenas em sistemas que possui ou que está explicitamente autorizado a
testar. O driver do laboratório recusa variantes que não sejam vulnerable/patched, o
scanner recusa alvos que não sejam loopback a menos que
--allow-authorized-non-loopback seja fornecido, e a porta do Compose está vinculada
a 127.0.0.1.
Consulte SECURITY.md para o tratamento de divulgação. Este repositório não inclui alvos de produção, dados de clientes, tokens de acesso ou evidência de sistemas reais.
MIT - consulte LICENSE.