
Pre-auth RCE in React Server Components versions 19.0.0, 19.1.0, 19.1.1, and 19.2.0.
This repository contains a PoC reproduction of CVE-2025-55182, a critical security vulnerability in React Server Components (RSC) that allows unauthenticated arbitrary code execution.
The vulnerability exists in how React Server Components deserialize "Server Actions" from client requests. Specifically, the requireModule function failed to validate that the requested export name was a direct prop of the module. This allowed attackers to access the constructor prop of exported functions, obtaining a reference to the global Function constructor, which can be used to execute arbitrary code.
This PoC uses a minimal Node.js environment to isolate the vuln in the react-server-dom-webpack library, just to make sure that the exploit demonstrates the bug in the library itself, NOT a misconfiguration in a framework.
npmnpm install
[!NOTE] The
package.jsonis pinned to the vulnerable version19.0.0.
This script sets up a raw HTTP server that uses the vulnerable React runtime to decode requests.
# tty1
node --conditions react-server server.js
In a separate terminal, run the exploit. This sends a malicious Flight payload to the server.
# tty2
node exploit.js id
You should see the command output returned in the response:
Expected Output:
Response: uid=0(root) gid=0(root) groups=0(root)
Why did the vulnerability happen?
The requireModule function in ReactFlightDOMServerNode.js basically just trusted whatever name the client sent. It did moduleExports[metadata[NAME]] without checking whether that property was actually meant to be exposed. So if the client said "bro, I want this property", the server just said "sure thing! here you go, dawg".
Why is letting people access any property a bad idea?
Because it basically lets anyone reach into the prototype chain, even the constructor, which is super dangerous. If the module happens to export a function (like module.exports = () => {}), then its constructor is literally the global Function constructor.
Why does getting the Function constructor mean RCE?
Once an attacker grabs the Function constructor, they can abuse the "Bound Server Action" feature. They bind a string containing malicious JavaScript to it (basically turning it into new Function("evil code")). And once that runs, the server executes whatever code they put in.
Why would React actually run that malicious function?
Because Server Actions can be triggered by ID. If the attacker crafts a payload with an Action ID that points to their module#constructor reference, React resolves it like a normal action and executes it. That "action" is actually their malicious function.
Why wasn't any of this validated?
The system just assumed that id and name from the Server Reference metadata would always refer to valid exports defined by the developer. There was no safety check like hasOwnProperty to make sure the requested prop was actually a real export and not something inherited from the prototype chain.
server.js instead of Next.js?I use a raw server.js (and a helper webpack-runtime.js) to manually configure the React Server Components runtime. This allows us to:
module.exports = fn)s. A real bundler might change how exports are wrapped, depending on its config tho.react-server-dom-webpack, not Next.js.react-server-dom-webpack assumes it's running inside a Webpack bundle. Our webpack-runtime.js gives it the globals it expects (__webpack_require__, __webpack_chunk_load__).
This is not mocking the vuln, it's just giving the library the bare minimum runtime it needs to actually work.There has been discussion about "Invalid PoCs" that only work if the developer purposely exposes dangerous stuff such as child_process.exec.
This PoC is not one of those. It works on a normal, safe setup.
The exposed function is harmless
The app exposes a simple updateProfile function that just returns a string and nothing sketchy, no shell commands.
The exploit fully escapes that function
The vulnerability lets the attacker ignore the safe export and jump straight to updateProfile.constructor, which is the global Function constructor.
The core issue is the property access
React should NOT have allowed access to .constructor. The developer did not intend to expose the Function constructor, instead, the insecure deserialization did that for them.
The only real requirement is that the module exports a function directly (module.exports = fn), which is super common in CommonJS and many bundler setups.
The payload in exploit.js crafts a React Flight message with three chunks:
id: "user-profile-action#constructor", meaning "give me the constructor".bound: points to Chunk 2, which contains the arguments.["console.log('nice try, diddy!')"]: the malicious code string.When React deserializes this:
user-profile-action..constructor property => getting the global Function.new Function("console.log('nice try, diddy!')")And that's the RCE!
Upgrade immediately to the patched versions:
react-server-dom-webpack >= 19.0.1react-server-dom-parcel >= 19.0.1react-server-dom-turbopack >= 19.0.1The patch introduces hasOwnProperty checks to prevent accessing inherited properties and restricts base64 file uploads.
If you run this PoC against a patched version, the server will crash or error with:
$ node --conditions react-server server.js
Listening on http://localhost:3000
/path/to/CVE-2025-55182/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js:2726
resolvedValue = resolvedValue.bind.apply(
^
TypeError: Cannot read properties of undefined (reading 'bind')
at /path/to/CVE-2025-55182/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js:2726:43
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
Node.js v20.19.3
This confirms that the exploit failed to access the constructor property (it returned undefined instead of Function), and thus the subsequent .bind call failed.
This code is for educational and testing purposes only. Do not use this exploit against systems you do not own or have explicit permission to test.
Released under DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE.