Execução remota de código sem autenticação no next/og do Next.js, do escape ausente à cadeia ROP
O next/og gera imagens de compartilhamento social convertendo JSX em SVG, com a biblioteca Satori, e depois transformando esse SVG em imagem. O Satori escreve o texto do usuário no SVG sem escapá-lo, então uma requisição consegue fechar o elemento e injetar a própria marcação. Quando a rota roda no runtime Node.js com a biblioteca sharp instalada, esse SVG é processado por bibliotecas nativas, e não pelo renderizador isolado em WebAssembly. Uma marcação com referência XInclude e entidades aninhadas corrompe a memória do analisador nativo; como o binário oficial do Node não é PIE, uma cadeia ROP fixa alcança a execução de comandos sem precisar vazar endereços. Uma única requisição, sem login, executa comandos no servidor.
O Satori escreve o texto controlado pelo usuário dentro do SVG sem escape (CWE-116), o que permite fechar o elemento que envolve o texto e injetar marcação SVG arbitrária [1] [4]. No caminho vulnerável (runtime Node.js com a biblioteca sharp presente), esse SVG chega a bibliotecas nativas de análise e rasterização, e não ao renderizador isolado em WebAssembly [1] [3]. Uma marcação com referência XInclude e entidades DTD aninhadas corrompe a memória desse analisador nativo, e uma cadeia ROP fixa, viável porque o binário oficial do Node não é PIE, alcança a execução de comandos sem qualquer vazamento de endereço [1]. Qualquer rota que passe dados da requisição para o ImageResponse é um ponto de injeção [1] [2].
O CVE-2026-94545 é uma falha de execução remota de código sem autenticação no next/og, o componente do Next.js que gera imagens de compartilhamento social (Open Graph) [1] [2]. A Vercel, mantenedora do Next.js, atribuiu ao problema uma pontuação CVSS v4 de 9.5, com exploração pela rede, baixa complexidade, sem autenticação e sem interação do usuário [2] [5]. A raiz é um defeito de escape de saída (CWE-116) na biblioteca Satori, que compõe o SVG a partir do JSX; a consequência, na configuração vulnerável, é a execução de comandos no servidor [1] [4].
A exploração encadeia quatro etapas [1]. Primeiro, a injeção de SVG: o texto do atacante fecha o elemento onde deveria ficar contido e insere marcação própria. Segundo, o alcance do parser nativo: com a biblioteca sharp instalada, o SVG é processado por libvips, librsvg e libxml2, e não pelo renderizador seguro em WebAssembly. Terceiro, a corrupção de memória: uma referência XInclude combinada com entidades DTD aninhadas corrompe a memória dentro do analisador. Quarto, o controle de execução: como o binário oficial do Node.js não é compilado como PIE (Position-Independent Executable), seus endereços de código são fixos, e uma cadeia ROP (Return-Oriented Programming) predefinida chama execve sem precisar descobrir endereço nenhum [1].
O impacto é execução de comandos com os privilégios do processo do servidor Next.js [1] [2]. O ataque é cego: ao ter sucesso, o comando substitui o processo trabalhador, então a resposta HTTP simplesmente encerra e o resultado precisa voltar por outro caminho, como um shell reverso [1]. O mesmo efeito derruba o trabalhador, e o servidor para de responder até ser reiniciado [1]. Como os endereços do binário do Node são estáveis, não há necessidade de vazamento nem de força bruta por alvo [1].
A correção definitiva é atualizar para Next.js 16.3.6 (com Satori 0.33.5 ou superior), que escapa o texto do SVG na serialização e fecha a injeção [1] [2]. As versões da linha 15.x do Next.js não são afetadas pela execução remota de código [2]. Este relatório inclui, na seção 7, a verificação de que a plataforma CiberLab não está exposta a essa falha.
O CiberLab não executou o exploit contra uma instância própria nem realizou engenharia reversa das bibliotecas nativas. A descrição da causa raiz e da cadeia de exploração vem da prova de conceito publicada pela equipe EQSTLab da SK Shieldus [1], que inclui o exploit, o ambiente de laboratório e a explicação técnica. Os metadados da vulnerabilidade (pontuação, versões, correção) vêm do comunicado da Vercel e de fontes de inteligência abertas verificadas [2] [3] [4] [5]. A verificação da seção 7, ao contrário, foi feita diretamente sobre o código-fonte da plataforma CiberLab.
| Campo | Valor |
|---|---|
| Identificador | CVE-2026-94545 |
| Avisos | GHSA-vcvr-r3jv-pc5j (Next.js) e GHSA-wx4j-mvgx-mqwp (Satori) [3] |
| Componente afetado | next/og (ImageResponse), sobre a biblioteca Satori [1] [2] |
| Classe de fraqueza | CWE-116 (Improper Encoding or Escaping of Output), levando a injeção de SVG e corrupção de memória nativa [1] [4] |
| Ponto de entrada | Qualquer rota que passe dados da requisição para o ImageResponse [1] [2] |
| Vetor de ataque | Rede, sem autenticação, sem interação do usuário [2] [5] |
| Pontuação CVSS v4 | 9.5 (Crítica), atribuída pela Vercel [2] [5] |
| Pré-condições | Runtime Node.js, biblioteca sharp instalada e entrada do usuário chegando ao ImageResponse [1] [2] |
| Correção | Next.js 16.3.6 e Satori 0.33.5 ou superior [1] [2] |
| Natureza do exploit | Cego (blind); a resposta HTTP encerra e o resultado retorna fora de banda [1] |
| Componente | Versões vulneráveis | Primeira versão corrigida |
|---|---|---|
| Next.js | 16.2.0 até anterior a 16.3.6 (runtime Node.js com sharp) [1] [2] | 16.3.6 [1] [2] |
| Satori | >= 0.0.27 e < 0.33.5 [1] | 0.33.5 [1] |
| Next.js 15.x | Não afetado pela execução remota de código [2] | Não se aplica [2] |
Apenas o caminho do runtime Node.js é afetado. Com export const runtime = 'edge', ou numa instalação sem a biblioteca sharp, a imagem é desenhada pelo renderizador isolado resvg em WebAssembly, e a injeção de SVG não leva à execução de código [1]. A distinção entre os dois renderizadores é o eixo de toda a análise.
Imagens Open Graph são as pré-visualizações que aparecem quando um link é compartilhado em uma rede social ou aplicativo de mensagens [2]. O Next.js oferece o next/og para gerá-las dinamicamente: o desenvolvedor descreve a imagem como se fosse um componente, em JSX, e o framework a transforma em uma imagem PNG [1] [2]. Essa conversão passa por três estágios, e a falha nasce na costura entre o segundo e o terceiro.
O primeiro estágio é o Satori, biblioteca da Vercel que converte a estrutura JSX (ou semelhante a HTML) em um documento SVG [1] [2]. É aqui que o texto fornecido pelo desenvolvedor, e eventualmente pelo usuário, é escrito dentro dos elementos do SVG [1]. O defeito está justamente neste ponto: o Satori não escapa esse texto ao serializá-lo (CWE-116) [1] [4].
O segundo estágio transforma o SVG em pixels, a rasterização, e o Next.js tem dois caminhos possíveis para isso [1]:
resvg compilado em WebAssembly. É um ambiente isolado (sandbox), no qual uma injeção de SVG não escapa para o sistema [1].SVG é XML, e o analisador libxml2 implementa recursos poderosos da especificação XML que, num contexto de segurança, são armadilhas [1]. Dois importam aqui: as entidades DTD (Document Type Definition), que permitem definir abreviações expansíveis dentro do documento, e o XInclude, que permite a um documento XML incluir o conteúdo de outro [1]. Quando o atacante controla a marcação SVG que chega a essa biblioteca, ele controla também o uso desses recursos, e é a combinação deles que corrompe a memória do analisador [1].
A falha não é um único bug, mas uma cadeia em que cada elo só é alcançável porque o anterior falhou [1]. Esta seção percorre a causa raiz; a seção 5 mostra a exploração concreta.
Quando o Satori serializa o SVG, o texto do usuário é inserido verbatim dentro de um elemento, por exemplo um <title> [1]. Sem o escape, os caracteres < e > do texto do atacante são interpretados como marcação, e não como conteúdo textual [1] [4]. Isso permite ao atacante encerrar prematuramente o elemento que o continha e escrever elementos SVG inteiramente novos, controlados por ele [1]. É o equivalente, no mundo do SVG, de uma injeção clássica: o dado ultrapassa a fronteira que deveria contê-lo e vira código de marcação.
A injeção de SVG, por si só, seria de baixo impacto se o SVG resultante fosse desenhado pelo renderizador isolado em WebAssembly [1]. O que eleva a falha à execução de código é a presença simultânea de duas condições: a rota rodar no runtime Node.js e a biblioteca sharp estar instalada [1] [2]. Juntas, elas desviam a rasterização para o caminho nativo (libvips, librsvg, libxml2), onde a marcação injetada é processada por código em C sem isolamento [1].
A mesma injeção de SVG é inofensiva no renderizador WebAssembly e catastrófica no nativo. Ao usar o next/og, não presuma que o caminho seguro está ativo: confirme o runtime da rota e a presença da biblioteca sharp. A conveniência de instalar a sharp para outras finalidades pode, sem que se perceba, ligar o caminho vulnerável de uma rota de imagem que parecia inócua [1].
Com o SVG malicioso alcançando a libxml2, o atacante monta uma estrutura que combina uma referência XInclude a um documento externo com entidades DTD aninhadas, cujo conteúdo se expande de forma controlada [1]. O processamento dessa estrutura corrompe a memória dentro do analisador nativo [1]. A prova de conceito da EQSTLab calibra o tamanho exato dessa expansão (com valores de preenchimento fixos) para dispor a memória de maneira previsível, um trabalho que depende das versões precisas das bibliotecas nativas [1].
Corromper a memória não basta: é preciso converter essa corrupção em controle da execução [1]. Aqui entra uma propriedade do próprio Node.js: o binário oficial distribuído pelo projeto não é compilado como PIE, ou seja, seu código não é carregado em endereços aleatórios (não há ASLR sobre a imagem principal) [1]. Isso torna os endereços das instruções, as gadgets, fixos e conhecidos para cada versão do Node [1].
Com endereços fixos, o atacante monta previamente uma cadeia ROP: uma sequência de endereços que reaproveita trechos do próprio binário para, passo a passo, montar uma chamada de sistema execve e executar um comando [1]. Como nada precisa ser descoberto em tempo de execução, não há necessidade de um vazamento de endereço nem de força bruta, e o mesmo payload funciona em qualquer host com aquela mesma compilação do Node [1].
A prova de conceito da EQSTLab reúne os elos numa única requisição HTTP contra a rota de imagem [1]. Esta seção mostra o formato do ataque e o comportamento observável, sem reproduzir a cadeia ROP em si.
O laboratório expõe uma rota que reproduz a configuração vulnerável: runtime Node.js e a entrada da requisição indo direto para um elemento de texto do SVG [1]. O trecho central é este:
export const runtime = 'nodejs'
// A entrada nao autenticada do usuario vai direto para o texto do SVG.
const value = request.method === 'POST'
? await request.text()
: (url.searchParams.get('value') ?? '')
return new ImageResponse(
<svg width="1200" height="630"><title>{value}</title></svg>,
{ width: 1200, height: 630 }
)
O valor de value, vindo do corpo da requisição ou da query string, é escrito no <title> do SVG sem qualquer tratamento [1]. É o ponto exato onde a injeção descrita na seção 4.1 acontece.
O exploit envia uma requisição POST /api/og com o corpo em texto simples [1]. Esse corpo, do ponto de vista da estrutura, faz três coisas: fecha o elemento de texto onde a entrada foi colocada, insere uma referência XInclude que aponta para um documento SVG malicioso (embutido como URL de dados), e reabre o elemento para manter o SVG bem formado [1]. O documento referenciado, por sua vez, carrega as entidades DTD aninhadas e a estrutura calibrada que dispara a corrupção [1]. A cadeia ROP é codificada dentro de um elemento <path>, cujas coordenadas numéricas representam os endereços das gadgets [1].
Ao ter sucesso, o execve substitui o processo trabalhador do Node pelo comando do atacante. A conexão HTTP, que pertencia àquele processo, simplesmente cai. Por isso o exploit é cego: não há corpo de resposta com o resultado. O atacante recupera a saída por um canal próprio, tipicamente um shell reverso que o comando injetado abre de volta para uma máquina sob seu controle [1].
Do lado do defensor, uma exploração bem-sucedida deixa três sinais [1]:
POST à rota de imagem com corpo grande (dezenas de milhares de bytes) contendo marcação SVG, referências xi:include e URLs de dados data:image/svg+xml [1].Nem toda aplicação Next.js está vulnerável. As quatro condições precisam valer ao mesmo tempo [1] [2]:
| Condição | Por que importa |
|---|---|
| Next.js 16.2.0 a 16.3.5 | Faixa de versões com o Satori vulnerável embutido; a 15.x e a 16.3.6 estão fora [1] [2] |
Rota usando next/og (ImageResponse) | É o componente que compõe o SVG; sem ele, não há sink [1] [2] |
| Runtime Node.js na rota | Sem o runtime Edge, a rasterização segue o caminho nativo, e não o WebAssembly isolado [1] |
| Biblioteca sharp instalada | É o que desvia a rasterização para libvips, librsvg e libxml2 [1] |
Entrada do usuário chegando ao ImageResponse | Sem dado controlado pelo atacante no SVG, não há injeção [1] [2] |
Onde as condições valem, o impacto é máximo [1] [2]:
A plataforma CiberLab é construída em Next.js, então foi verificada diretamente contra as condições da tabela 3. A conclusão é que a plataforma não está vulnerável ao CVE-2026-94545, e por mais de um motivo independente [1] [2].
| Condição | Estado no CiberLab | Resultado |
|---|---|---|
| Versão do Next.js na faixa 16.2.0 a 16.3.5 | A plataforma roda Next.js 15.5.25, fora da faixa vulnerável [2] | Não atendida |
Uso de next/og ou ImageResponse | Busca no código: nenhuma importação de next/og, ImageResponse ou @vercel/og; sem a dependência satori | Não atendida |
| Rota de imagem OG dinâmica | Não existe rota /api/og nem equivalente; a imagem de compartilhamento é um arquivo PNG estático (/images/og-ciberlab.png) | Não atendida |
| Biblioteca sharp com entrada do usuário | A sharp está presente, mas usada apenas em dois scripts de build, sobre SVG estático próprio; nunca em um manipulador de requisição nem com dado do usuário | Não atendida |
Bastaria uma das condições da tabela 4 falhar para afastar a falha. No CiberLab, todas falham: a versão está fora da faixa, não há next/og, não há rota de imagem dinâmica, e a única presença da biblioteca sharp é em tempo de compilação, sobre conteúdo estático da própria plataforma. A imagem Open Graph do site é gerada uma vez, no build, a partir do conteúdo do próprio site, e servida como um arquivo fixo, sem qualquer caminho por onde um dado de requisição chegue a um rasterizador.
Recomenda-se manter essa postura em futuras evoluções da plataforma: se algum dia uma imagem Open Graph dinâmica for desejada, ela deve usar o runtime Edge (export const runtime = 'edge') ou escapar rigorosamente qualquer dado de usuário antes de compor a imagem, e a versão do Next.js deve permanecer atualizada [1] [2].
A detecção vai da inspeção mais barata (a requisição na borda) para a mais cara (a atividade posterior no host) [1]. O padrão da requisição de exploração é distintivo e não aparece em uso legítimo de uma rota de imagem [1].
Uma requisição legítima a uma rota next/og é curta e não carrega marcação XML no corpo [1]. A requisição de exploração, ao contrário, tem um corpo grande, de dezenas de milhares de bytes, que contém marcação SVG, referências xi:include e URLs de dados data:image/svg+xml [1]. Esses três elementos juntos, numa requisição a uma rota de imagem, são um sinal de alta confiança.
Para sensores que inspecionam o corpo HTTP antes da terminação TLS, a regra abaixo alerta um corpo grande com os marcadores da injeção em uma requisição a rota de imagem [1]:
alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"CIBERLAB NEXTJS OG CVE-2026-94545 SVG INJECTION RCE ATTEMPT"; \
flow:to_server,established; http.method; content:"POST"; \
http.request_body; content:"xi:include"; nocase; content:"data:image/svg+xml"; nocase; distance:0; \
content:"XInclude"; nocase; \
classtype:web-application-attack; sid:1000120; rev:1; \
reference:cve,2026-94545; reference:url,ciberlab.seg.br/reports/nextjs-og-rce;)
A regra Sigma a seguir opera sobre registros de proxy ou de aplicação que exponham o caminho requisitado e o tamanho do corpo [1]:
title: Tentativa de RCE no next/og do Next.js (CVE-2026-94545)
id: 86abcdef-012d-4c24-b514-795d346d6592
status: experimental
description: Detecta POST a rota de imagem OG com corpo grande e marcadores de injecao de SVG
author: Lucas Rayan Guerra (CiberLab)
references:
- https://github.com/EQSTLab/CVE-2026-94545
- https://github.com/vercel/next.js/security/advisories/GHSA-vcvr-r3jv-pc5j
logsource:
category: webserver
detection:
selection_method:
cs-method: 'POST'
selection_body:
cs-body|contains:
- 'xi:include'
- 'data:image/svg+xml'
condition: selection_method and selection_body
fields:
- c-ip
- cs-uri-stem
- cs-bytes
- sc-status
falsepositives:
- Desconhecidos em rotas de imagem legitimas
level: high
tags:
- attack.initial_access
- attack.t1190
- attack.execution
- attack.t1059.004
No servidor, três observações confirmam o sucesso [1]. Reinícios inesperados ou quedas do trabalhador do Next.js logo após uma requisição à rota de imagem indicam a substituição do processo [1]. Um processo filho de shell (por exemplo bash ou sh) descendente do processo do Node é altamente anômalo em um servidor web e sugere execução de comando [1]. E uma conexão de saída do servidor para um endereço externo, sobretudo em porta não padronizada, é o traço do shell reverso [1]. Correlacione os três com o registro de acesso da rota de imagem.
A solução completa é atualizar o Next.js para 16.3.6 ou superior (que traz o Satori 0.33.5 ou superior) e reimplantar [1] [2]. A correção escapa o texto do SVG ao serializá-lo, o que fecha a injeção na origem, antes de o SVG chegar a qualquer rasterizador [1].
Se a atualização imediata não for possível, qualquer uma das medidas abaixo interrompe a cadeia [1]:
export const runtime = 'edge' nas rotas next/og, para forçar o rasterizador isolado em WebAssembly [1].ImageResponse; se algum texto de usuário precisar aparecer na imagem, escape-o rigorosamente antes [1].Se houver indício de exploração bem-sucedida, trate o servidor como comprometido [1]. Gire os segredos que o processo da aplicação podia ler (variáveis de ambiente, chaves de API, credenciais de banco e de serviços), procure mecanismos de persistência instalados pelo atacante, e reconstrua a instância a partir de uma imagem limpa em vez de tentar higienizá-la no lugar [1]. Reveja os registros de saída em busca de conexões para endereços externos após a requisição suspeita.
IDs e nomes conferidos na base oficial do MITRE ATT&CK for Enterprise [6]:
| Técnica | Nome | Relação com o caso |
|---|---|---|
| T1190 | Exploit Public-Facing Application | Exploração da rota de imagem OG exposta, sem autenticação [1] [2] |
| T1059.004 | Command and Scripting Interpreter: Unix Shell | Comando de shell executado via execve pela cadeia ROP [1] |
| T1571 | Non-Standard Port | Shell reverso de saída para uma porta não padronizada [1] |
| T1005 | Data from Local System | Leitura de segredos e arquivos acessíveis ao processo do servidor após a execução [1] [2] |
| T1499.004 | Endpoint Denial of Service: Application or System Exploitation | Substituição do trabalhador, que derruba o servidor até o reinício [1] |
| T1588.005 | Obtain Capabilities: Exploits | Uso da cadeia ROP e do exploit público calibrado para a pilha do alvo [1] |
Os indicadores são de comportamento: o ataque usa requisições HTTP e não deposita um binário conhecido no servidor [1]. A tabela consolida os padrões a caçar na borda e no host [1].
| Tipo | Indicador | Severidade | Contexto e ação |
|---|---|---|---|
| Vulnerabilidade | CVE-2026-94545 | Crítica | RCE sem autenticação no next/og. Atualize para Next.js 16.3.6 ou superior [1] [2]. |
| Padrão na requisição | xi:include + data:image/svg+xml | Crítica | Marcadores da injeção de SVG num POST a rota de imagem; nenhum cliente legítimo os envia [1]. |
| Padrão na requisição | POST /api/og com corpo > 20 KB | Alta | Corpo anormalmente grande em rota de imagem OG; a prova de conceito gera cerca de 24 KB [1]. |
| Comportamento no host | shell filho do processo Node | Crítica | Processo bash/sh descendente do Node indica execução de comando [1]. |
| Comportamento no host | queda/reinício do worker após POST | Alta | Substituição do trabalhador logo após uma requisição à rota de imagem [1]. |
| Rede | conexão de saída em porta não padronizada | Alta | Traço de shell reverso a partir do servidor após a exploração [1]. |
CVE-2026-94545, execução remota de código no next/og do Next.js. Relatório técnico de análise de vulnerabilidade, versão 1.0, emitido em 29 de setembro de 2026.
Lucas Rayan Guerra, CiberLab, uma iniciativa do Ciência Embarcada.
Classificado TLP:CLEAR: distribuição livre, sem restrição de compartilhamento.