CVE-2025-55182 ·
React2Shell: unauthenticated RCE on vanilla Next.js, and what I got wrong first
A research log of CVE-2025-55182, including the wrong first conclusion and the piece that was missing.
Important Disclaimer
This document describes my personal research process into CVE-2025-55182, including confirmed findings and experiments I carried out. This represents an honest account of my investigation, including initial incorrect assumptions and the eventual breakthrough.
Executive Summary
After an extensive investigation into CVE-2025-55182 (CVSS 10.0), my initial conclusion was that automatic RCE had not been publicly demonstrated and that exploitation required application-specific gadgets. This conclusion was incorrect.
On December 5, 2025, after receiving additional insights from another independent researcher on X (@maple3142), I successfully reproduced full unauthenticated RCE on vanilla Next.js without requiring any application-specific code vulnerabilities.
Research Methodology
Phase 1: Patch Analysis (Initial Incorrect Assumption)
My initial focus was on the first change in the React 19.0.1 patch:
Vulnerable (19.0.0):
return fn.bind.apply(fn, [null].concat(_ref));Patched (19.0.1):
if (Array.isArray(promiseValue)) {
promiseValue = promiseValue.slice(0);
} else {
promiseValue = [];
}I assumed the attack path was through fn.bind.apply() with malicious objects instead of arrays. I was able to demonstrate argument injection into Server Actions using $ACTION_REF_ with attacker-controlled bound:
curl -X POST http://localhost:9000/ \
-F '$ACTION_REF_0=' \
-F '$ACTION_0:0={"id":"<ACTION_ID>","bound":["; id #","/etc/passwd"]}'Result: The arguments were successfully injected into the Server Action. However, this only leads to RCE if the target function uses those arguments unsafely.
Phase 2: Unsafe Behavior Observed in the Patch
A deeper look at the patch revealed another important change in getOutlinedModel():
Vulnerable:
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]];Patched:
if (hasOwnProperty.call(value, name)) {
value = value[name];
}This vulnerable behavior allowed prototype-chain traversal using references like:
$1:__proto__:constructor:constructorPhase 3: Experiment with Thenable and Function.constructor (Dead End)
During the research, I tested a thenable object containing a .then property:
{"then": "$1:__proto__:constructor:constructor"}When JavaScript processes this through await:
- JavaScript sees a
.thenproperty and treats the object as a Promise - Calls
obj.then(resolve, reject) - If
thenresolves toFunction.constructor, JavaScript attempts to executeFunction(resolve, reject)
Observed result:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>)Phase 4: The Argument Binding Limitation (The Wall)
When Function.constructor is invoked as:
Function(resolve, reject)
// resolve.toString() = "function () { [native code] }"
// Function attempts to parse this as code → SyntaxErrorThe arguments resolve and reject are always the native Promise functions. Function attempts to interpret the first argument as source code, which is invalid JavaScript.
This is where my research stalled. I concluded that controlling arguments to Function.constructor was impossible without an application-specific gadget.
Phase 5: The Breakthrough — Blob Deserialization (December 5, 2025)
After publishing my initial findings, another independent researcher pointed me to a critical piece I had missed: the $B (Blob) deserialization sink.
The Missing Piece
In the compiled React Flight server code (not visible in TypeScript sources), there exists:
case "B":
return response._formData.get(response._prefix + id);Location:
- Package:
react-server-dom-webpack@19.0.0 - File:
cjs/react-server-dom-webpack-server.node.unbundled.development.js - Also in:
next@15.0.3/dist/compiled/react-server-dom-webpack/
This code allows React to call response._formData.get() with values derived from attacker-controlled input, without validation.
Why This Changes Everything
| Approach | Arguments to Function.constructor | Result |
|---|---|---|
| Thenable (Phase 3) | resolve, reject (native functions) | SyntaxError |
| Blob + Poisoned Response | _prefix (attacker-controlled string) | RCE |
By combining:
- Prototype traversal (
$1:__proto__:then→Chunk.prototype.then) - A poisoned
_responseobject with:_formData.getset toFunction.constructor_prefixset to arbitrary JavaScript code
- An inner model containing
$Breference
The case "B": handler executes:
Function.constructor("<attacker code>" + id)This bypasses the argument binding limitation entirely.
Public PoCs: Analysis
Popular GitHub PoCs claiming RCE use Action IDs such as:
"child_process#execSync""vm#runInThisContext"
These are fake. Next.js only accepts Action IDs defined by the application. Invalid IDs produce:
TypeError: Cannot read properties of undefined (reading 'workers')However, the real exploit does not require fake Action IDs. Any valid Server Action ID works.
Verification and Impact
I tested this exploitation chain on a minimal Next.js 15.0.3 + React 19.0.0 application with only:
async function myAction(data) {
"use server";
console.log("Server Action called with:", data);
return { success: true, received: data };
}Result: Full RCE confirmed. The application contained no unsafe code, no eval, no execSync, no gadgets.
Impact Assessment
| Aspect | Finding |
|---|---|
| Authentication required? | No |
| Application gadget required? | No |
| Works on vanilla Next.js? | Yes |
| Number of requests needed | 1 POST |
| Affected versions | Next.js ≤15.0.4 + React 19.0.0 |
| CVSS Score | 10.0 (justified) |
Critical Discovery: Patch Status
As of December 5, 2025, the $B sink still exists in Next.js 15.0.5 (the supposedly patched version).
Verification:
$ npm pack next@15.0.5
$ tar -xzf next-15.0.5.tgz
$ grep -A5 'case "B":' package/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.jsResult:
case "B":
return response._formData.get(response._prefix + obj);The code is identical to the vulnerable version.
Responsible Disclosure Note
Due to the discovery that the vulnerability may not be fully patched in the claimed "fixed" versions, I am withholding the complete proof-of-concept payload pending verification with the security teams at Vercel and Meta.
The technical details provided in this document are sufficient to understand the vulnerability mechanism but intentionally incomplete to prevent immediate exploitation.
Updated Conclusions
What I Found (Phases 1-4)
| Technique | Status |
|---|---|
Argument injection via bound | Confirmed (limited impact) |
| Prototype traversal | Confirmed |
Access to Function.constructor via thenable | Confirmed (but not exploitable alone) |
| Detection of vulnerable versions | Confirmed |
What I Missed Initially
| Issue | Impact |
|---|---|
The $B (Blob) deserialization sink | Critical - enables argument control |
| Examining compiled vs. source code | The sink only exists in compiled output |
| Response object poisoning mechanism | Allows bypassing all protections |
Final Assessment
CVE-2025-55182 is exploitable for full unauthenticated RCE on vanilla Next.js applications.
- No application-specific gadget required
- Works with a single HTTP POST
- The "gadget" is built into React's deserialization logic
- May not be fully patched in claimed fixed versions
Timeline
- December 3, 2025: Analysis of prototype traversal and thenable approach (dead end)
- December 4, 2025: Received insight about
$Bdeserialization from independent researcher - December 5, 2025: Full RCE reproduction confirmed
- December 5, 2025: Discovered vulnerability may persist in "patched" versions
- December 5, 2025: This report published (with PoC details withheld)
Recommendations
- Update immediately to the latest versions of React and Next.js
- Verify the patch by testing if
$Bdeserialization still accepts arbitrary_responseobjects - Monitor for exploitation attempts - look for:
- Unusual
Next-Actionheaders - Complex multipart payloads with
$@,__proto__,$Bpatterns
- Unusual
- Consider WAF rules to block suspicious patterns in Server Action requests
- Contact security teams if you're running affected versions in production
Acknowledgments
- The breakthrough insight regarding
$Bdeserialization was provided by an researcher on X (@maple3142) - The React and Next.js security teams for their work on patches (ongoing verification)
- The security research community for collaborative investigation
Lessons Learned
- Examine compiled code, not just sources - Critical vulnerabilities can hide in bundled output
- Revisit assumptions when new information emerges
- Collaborative research is essential for complex vulnerabilities
- Document the journey - Dead ends are valuable for understanding the full picture
- Responsible disclosure takes precedence over public recognition
Last Updated: December 5, 2025