By Durgesh Pawar

The modern web development landscape is experiencing a paradigm shift. With the widespread adoption of React Server Components (RSCs) and frameworks like Next.js, applications no longer merely ship static HTML or isolated JSON blocks over the network. Instead, they stream rich, interactive component trees directly from the server using a custom, line-delimited protocol known as Flight.

While Flight enables seamless progressive hydration, asynchronous data loading, and efficient server-driven code splitting, it introduces a profound architectural risk: it is a complex deserialization engine. When a network protocol moves beyond mere data transport to reconstruct executable behaviors, code-loading directives, and asynchronous promises, it creates a dangerous attack surface.

This structural vulnerability was laid bare by CVE-2025-55182—dubbed React2Shell—a CVSS 10.0 unauthenticated remote code execution (RCE) vulnerability residing within the Flight deserialization layer. This article breaks down the mechanics of the Flight protocol, explores how the vulnerability was weaponized by advanced threat actors, examines the cascading wave of follow-up bugs, and outlines a ranked, practical set of defenses to secure modern React applications.


Flight on the Wire: Understanding the Streaming Protocol

To comprehend why the Flight protocol represents a unique security surface, one must first look at what actually travels across the network. If you open your browser’s Network tab on any modern React Server Components application and inspect requests returning a Content-Type: text/x-component header, you are looking at Flight in action.

Unlike traditional API endpoints that return a single JSON object, Flight is a streaming, line-delimited format. Each line represents a self-contained "row" processed sequentially by the client-side React runtime as it arrives over the connection. Consider this simplified example of a Flight payload:

1:I["./src/components/ClientComponent.js",["chunks/main.js"],"default"]
2:J["$","article",null,"children":"$1"]
0:D"name":"RootLayout","env":"Server"
  • Row 1 acts as an import directive, instructing the client runtime to load ClientComponent.js from the application’s chunk map.
  • Row 2 is a serialized JSON tree constructing an HTML <article> element, where the string "$1" inside the children property points back to chunk 1.
  • Row 0 establishes the server execution context, identifying the component as a RootLayout executing in a server environment.

The Row Format and Prefix System

Every row strictly follows the syntax <ROW_ID>:<ROW_TAG><PAYLOAD>n. The row tag dictates how the parser interprets the payload, ranging from virtual DOM nodes (J) and module metadata (M) to preload hints (HL) and error boundaries (E).

However, the core complexity and security implications lie within the $ prefix system handled by parseModelString inside ReactFlightClient.js. When the client-side parser encounters a string starting with $, it routes the value through a specialized resolution path:

  • $ (Model Reference): Resolves to another chunk ID in the stream.
  • $ (Property Access): Recursively traverses properties on a resolved chunk (e.g., $1:user:name).
  • $S (Symbol): Creates a native JavaScript Symbol.
  • $F (Server Reference): Represents a callable Server Action (an RPC endpoint).
  • $L (Lazy Component): Defers component loading.
  • $@ (Promise/Raw Chunk): Returns the internal raw Chunk wrapper object instead of its resolved value.
  • $B (Blob/Binary): Triggers binary data deserialization.

The protocol does not merely deliver inert data; it instructs the client runtime on what code to load, which functions to call, and how to construct asynchronous promise chains.


Why Flight is a Deserialization Sink

In traditional software engineering, the risks of insecure deserialization are well-documented: Java’s ObjectInputStream spawned ysoserial, Python’s pickle executes arbitrary code on load, and PHP’s unserialize allows object-injection chains.

JavaScript developers have historically felt insulated from these risks. Native JSON.parse() only yields plain data objects; constructors do not fire, and magic methods do not run. However, the moment a framework wraps a custom parsing engine around JSON to reconstruct behavior, that insulation disappears.

Prototype Pollution via Property Traversal

JavaScript relies on prototype-based inheritance, where objects inherit properties through a hidden __proto__ link. Flight’s $:, property-access prefix performs deep traversal on deserialized objects by iterating through colon-separated path segments.

If an attacker crafts a payload utilizing segments like __proto__ or constructor, the traversal mechanism walks straight up the object prototype chain. Without strict ownership checks, this pattern enables classic prototype pollution.

Duck Typing and Thenables

The V8 JavaScript engine treats any object containing a .then property as a Thenable. During asynchronous execution, the runtime automatically checks for and invokes .then if it exists. Because Flight resolves chunks asynchronously, injecting a manipulated Thenable into the resolution pipeline forces the runtime to execute attacker-controlled code during standard await operations.


Chronology and Mechanics of React2Shell (CVE-2025-55182)

Disclosed in December 2025, CVE-2025-55182 sent shockwaves through the web security community. It was a CVSS 10.0 unauthenticated RCE vulnerability sitting squarely in the Flight server-side reply handling logic (ReactFlightReplyServer.js).

The Root Cause

The vulnerability originated in getOutlinedModel, specifically within the path-resolution loop for the $:, prefix system:

for (key = 1; key < reference.length; key++)
    parentObject = parentObject[reference[key]];

Notice the complete absence of a hasOwnProperty check. The parser accepted path segments directly from the network stream and applied them to the parent object without validating whether the property existed natively or resided higher up the prototype chain.

An attacker could supply a path such as $1:__proto__:constructor:constructor, forcing the traversal loop to move from a standard JSON object up through Object.prototype, into the Object constructor, and finally to the global Function constructor. In JavaScript, the Function constructor behaves identically to eval(), allowing the evaluation of arbitrary code: Function("malicious payload")().

Exploitation in the Wild

The exploitation window following the disclosure was practically nonexistent. Security researchers and telemetry from firms like Sysdig and Palo Alto Networks Unit 42 revealed immediate, in-the-wild exploitation by state-sponsored cyberespionage groups, most notably North Korean actors.

Weaponizing And Defending The React Flight Protocol: Deserialization Sinks In RSCs — Smashing Magazine

Attackers utilized the vulnerability to deploy stealthy, fileless implants. Sysdig linked the exploits to EtherRAT, a novel malware variant utilizing the Ethereum blockchain for command-and-control (C2) communications—a technique dubbed "EtherHiding" that renders server-side takedowns nearly impossible. Simultaneously, Unit 42 documented KSwapDoor, a backdoor disguised as a core Linux kernel swap daemon ([kswapd1]) that leveraged RC4 string encryption and AES-256-CFB communications over a P2P mesh network.


Official Responses and the Cascading Aftermath

The React core team responded rapidly, shipping patches across React 19 releases. The immediate patch implemented a cached, bulletproof ownership check at module load time:

var hasOwnProperty = Object.prototype.hasOwnProperty;
// Later in traversal paths:
hasOwnProperty.call(value, i);

By caching the original prototype method, the patch successfully blocked prototype-chain traversal via shadowed properties. However, this fix was merely reactive; it closed the specific gadget chain while leaving the underlying property-traversal model intact.

The Vulnerability Tail

The security audits triggered by React2Shell uncovered a series of subsequent vulnerabilities in the Flight deserialization pipeline:

CVE Identifier CVSS Type Description Fixed In
CVE-2025-55184 7.5 DoS Infinite recursion via nested Promises in Server Function deserialization. 19.0.2, 19.1.3, 19.2.2
CVE-2025-67779 7.5 DoS Incomplete initial fix for nested Promise recursion vector. 19.0.4, 19.1.5, 19.2.4
CVE-2026-23864 7.5 DoS / OOM Unbounded request body buffering and zipbomb-style decompression. 19.0.4+, 19.1.5+, 19.2.4+
CVE-2025-55183 5.3 Info Disclosure Crafted requests trigger implicit argument stringification, leaking source code. 19.0.1, 19.1.2, 19.2.1
CVE-2026-27978 5.3 CSRF Bypass Next.js treated Origin: null from sandboxed iframes as missing rather than cross-origin. Next.js 16.1.7

These follow-up disclosures highlight a fundamental truth: securing complex parsers that process untrusted network streams is an ongoing challenge. Denial-of-service vectors involving memory exhaustion (CVE-2026-23864) and information disclosure via source-code reflection (CVE-2025-55183) proved that patching a single RCE gadget chain does not eliminate architectural exposure.


Ranked, Practical Defenses for React Applications

Relying exclusively on framework-level patches is insufficient. Engineering teams must implement a robust, defense-in-depth strategy to mitigate structural deserialization risks. Below is a ranked list of practical defenses, ordered by impact.

1. Strict Input Validation on Server Actions (Zod, Valibot)

Because the Flight deserializer processes raw, unvalidated network data before your business logic executes, rigorous schema validation is your primary line of defense.

Validation must occur at the absolute beginning of every Server Action—before logging or any other operation. Logging unvalidated arguments can inadvertently trigger source code reflection bugs (CVE-2025-55183) before validation logic runs.

"use server"
import  z  from "zod"

const ProfileUpdateSchema = z.object(
  name: z.string().min(1).max(100),
  email: z.string().email(),
  role: z.enum(["user", "admin"]),
)

export async function updateProfile(data: unknown) 
  // Validate raw input immediately using safeParse
  const parsed = ProfileUpdateSchema.safeParse(data)
  if (!parsed.success) 
    return  error: "Invalid input structure" 
  

  // Proceed exclusively with validated data
  await db.users.update( data: parsed.data )

Crucial Rule: Avoid destructuring object arguments before validation. Accessing properties on unvalidated inputs opens the door to deserialization exploitation vectors.

2. The server-only Package

To prevent sensitive modules containing database queries, API secrets, or internal algorithms from being accidentally bundled or imported into client components, enforce the server-only package at the top of sensitive files:

import "server-only"
import  secureDatabasePool  from "@/lib/db"

export async function fetchInternalMetrics() 
  return secureDatabasePool.query("SELECT * FROM financials")

Be wary of barrel files (index.ts re-exports), which can transitively expose server modules to client components if boundaries are improperly managed. Note also that server-only protects code, not data return values; you must still manually filter data payloads returned to client components.

3. CSRF Hardening Beyond Framework Defaults

Following CVE-2026-27978, relying solely on default header checks is risky. For state-changing Server Actions:

  • Enforce explicit cookie configurations (SameSite=Strict or Lax).
  • Implement per-session cryptographic CSRF tokens for high-value transactions.
  • Never add 'null' to experimental.serverActions.allowedOrigins in your Next.js configuration, as this reopens cross-site request forgery vectors via sandboxed iframes.

4. Continuous Dependency Auditing

Verify that your lockfile strictly enforces patched React versions (19.0.4+, 19.1.5+, 19.2.4+ or higher) to protect against both RCE and memory-exhaustion DoS variants.

5. Leveraging the Taint API

React’s experimental taintObjectReference and taintUniqueValue utilities provide valuable development-time guardrails:

import  experimental_taintObjectReference as taintObjectReference  from "react"
import "server-only"

export async function getSecureUser(id: string) 
  const user = await db.users.findUnique( where:  id  )
  taintObjectReference("Do not pass raw user records to client components.", user)
  return user

Note: Taint tracking operates on object references rather than data content. Data transformations, object spreading, or JSON serialization round-trips will strip the taint. Treat this as a defense-in-depth utility, not an impenetrable security boundary.


Implications and Future Outlook

The vulnerability lifecycle of the React Flight protocol demonstrates a recurring historical pattern in software engineering. From Google Web Toolkit’s custom RPC serialization to Java Server Faces and ASP.NET ViewState vulnerabilities, frameworks that invent custom wire formats to bridge server and client environments consistently encounter deserialization challenges.

As the industry moves aggressively toward server-driven UI architectures, relying on the assumption that "the server is fully trusted" is no longer viable. Securing modern applications requires moving beyond reactive patching toward stronger architectural primitives: cryptographic validation of serialized payloads, signed component trees, and strict content-integrity monitoring of the Flight data stream itself.

Until the ecosystem evolves these native primitives, developers must maintain absolute vigilance—treating every Server Action input as hostile, strictly validating schemas at runtime, and recognizing that the framework’s internal plumbing requires active, defensive oversight.