Executive Overview
Modern web architecture has evolved past the traditional boundaries of stateless HTTP requests returning static HTML documents or clean JSON APIs. Today’s frameworks—pioneered heavily by React Server Components (RSC) and popularized by architectures like the Next.js App Router—rely on advanced serialization formats to stream rich, interactive component trees directly from server to browser.
At the heart of this paradigm lies Flight, a custom, line-delimited streaming protocol designed to handle module imports, asynchronous states, lazy components, and Remote Procedure Call (RPC) server actions. While Flight unlocks unprecedented performance optimizations and developer ergonomics, it simultaneously introduces complex structural risks.
Security researcher Durgesh Pawar’s deep dive into the protocol’s mechanics brings to light how manipulation of this deserialization layer led to CVE-2025-55182—a CVSS 10.0, unauthenticated remote code execution (RCE) vulnerability dubbed "React2Shell."
This article explores the technical anatomy of the Flight protocol, the nature of its deserialization sinks, the real-world exploitation campaigns launched by state-sponsored actors, and a rigorous, ranked hierarchy of defensive strategies for engineering teams securing modern React ecosystems.
Detailed Chronology: From Discovery to Exploitation
The security implications of server-driven UI streaming mechanisms crystallized in December 2025, sending shockwaves through the JavaScript ecosystem.
December 2025: The Disclosure of React2Shell (CVE-2025-55182)
When the React development team released security advisories regarding CVE-2025-55182, the severity score instantly commanded the industry’s undivided attention: a maximum CVSS rating of 10.0. The vulnerability required zero authentication, meaning any external actor could send a single, maliciously crafted HTTP request to a Server Function endpoint and gain immediate shell access to the underlying server.
The Cybersecurity and Infrastructure Security Agency (CISA) promptly placed the vulnerability onto its Known Exploited Vulnerabilities catalog, signaling an active, high-priority threat landscape.
In-the-Wild Exploitation: EtherRAT and KSwapDoor
Threat intelligence agencies quickly tied the vulnerability to sophisticated, state-sponsored campaigns. Security researchers at Sysdig discovered that North Korean threat actors were actively exploiting React2Shell in the wild to deploy EtherRAT—a novel, file-less malware implant leveraging the Ethereum blockchain for command-and-control (C2) communications, a technique colloquially known as "EtherHiding." Because the C2 infrastructure relied on public blockchains, traditional infrastructure takedowns proved ineffective.
Concurrently, Palo Alto Networks’ Unit 42 documented another active backdoor, KSwapDoor, masquerading as [kswapd1] on compromised Linux systems to blend into process lists alongside the legitimate kernel swap daemon. KSwapDoor utilized RC4 string encryption and AES-256-CFB mesh networking over Diffie-Hellman key exchanges, underscoring the velocity and engineering maturity with which bad actors weaponize protocol-level deserialization flaws.
Subsequent Disclosures and Patch Iterations
Following the initial React2Shell patch, subsequent code audits uncovered a secondary wave of vulnerabilities residing in the same deserialization surface:
- CVE-2025-55184 & CVE-2025-67779 (CVSS 7.5): Denial-of-service (DoS) vulnerabilities caused by infinite recursion of nested Promises within Server Function deserialization, effectively locking the Node.js event loop.
- CVE-2026-23864 (CVSS 7.5): An unbounded request body buffering and zipbomb-style decompression vector leading to memory exhaustion and server crashes.
- CVE-2025-55183 (CVSS 5.3): An information disclosure flaw where crafted requests forced Server Functions to reflect internal source code when stringifying arguments.
- CVE-2026-27978 (CVSS 5.3): A cross-site request forgery (CSRF) bypass where Next.js incorrectly parsed sandboxed iframe
Origin: nullheaders as missing rather than cross-origin.
Flight On The Wire: Understanding the Streaming Protocol
To understand how these vulnerabilities manifest, one must examine what actually travels over the wire when an RSC page renders.
Unlike traditional APIs, a Next.js App Router response returning Content-Type: text/x-component is not a single JSON payload. Instead, it is a streaming, line-delimited format where each line represents an independent "row" processed sequentially by the client-side React runtime.
A typical Flight payload resembles the following structure:
1:I["./src/components/ClientComponent.js",["chunks/main.js"],"default"]
2:J["$","article",null,"children":"$1"]
0:D"name":"RootLayout","env":"Server"
- Row 1 (
I– Import): Instructs the client to asynchronously load a specific client component module from the bundler chunk map. - Row 2 (
J– JSON Tree): Constructs virtual DOM structures, component props, and HTML elements. The"$1"reference points directly back to chunk 1. - Row 0 (
D– Data): Defines server execution contexts and environment metadata.
The Prefix System ($)
The true complexity—and risk—of Flight lies within its prefix resolution parser (parseModelString in ReactFlightClient.js). When encountering a string beginning with $, the parser intercepts it and evaluates a specialized type system:
| Prefix | Type | Parser Behavior |
|---|---|---|
$ |
Model Reference | Resolves to another chunk ID in the active data stream. |
$: |
Property Access | Traverses nested properties across resolved chunks (e.g., $1:user:name). |
$S |
Symbol | Instantiates a native JavaScript Symbol. |
$F |
Server Reference | Establishes a callable Server Action (an RPC endpoint). |
$L |
Lazy Component | Defers component loading until rendered. |
$@ |
Promise / Raw Chunk | Exposes internal Chunk wrapper objects and Thenable metadata. |
$B |
Blob / Binary | Triggers binary deserialization handlers. |
Crucially, Flight is not merely a data-interchange format like JSON; it transmits behavior. It dictates code-loading instructions, RPC endpoints, and asynchronous execution chains before application-level logic executes.
Supporting Context & Metrics: The Mechanics of the Deserialization Sink
In computer science history, insecure deserialization is a recurring architectural anti-pattern. Java’s ObjectInputStream gave rise to ysoserial, Python’s pickle executes arbitrary code on load(), and PHP’s unserialize chains magic execution methods.
JavaScript developers have historically assumed safety via JSON.parse(), which produces inert data structures without invoking constructors or magic methods. However, the moment a framework wraps custom parsing layers around data streams to reconstitute behavioral objects, that safety guarantee dissolves.
The Prototype Pollution Vector ($:)
The $:, or property access prefix, allows data streams to specify deep property paths like $1:user:name. The underlying resolution engine (getOutlinedModel) iterates through these colon-separated segments, applying them directly to parent objects without validating ownership:

for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]];
By injecting segments such as __proto__:constructor:constructor, an attacker forces the loop to traverse from a standard JSON object up through Object.prototype, into the Object constructor, and finally to the Function constructor. In V8 and JavaScript environments, the Function constructor behaves identically to eval(), instantly converting string traversal into arbitrary remote code execution.
Official Statements and Framework Patches
The React core team responded to CVE-2025-55182 by hardening the property ownership validation layer. Rather than dismantling the protocol’s structural traversal design, the patch explicitly caches the native hasOwnProperty method at module load time:
var hasOwnProperty = Object.prototype.hasOwnProperty;
// Downstream checks use the prototype method explicitly:
hasOwnProperty.call(value, i);
By forcing .call() against the native prototype method, the patch blocks object shadowing attacks, successfully neutralizing the known gadget chain. This fix was backported across React versions 19.0.1, 19.1.2, and 19.2.1.
However, security analysts note that while the patch successfully blocks known exploits, the underlying architectural pattern—exposing property traversal and behavioral reconstruction over network streams—remains a persistent surface area requiring rigorous secondary defenses.
Ranked Defenses for React Server Components
Because the framework patch targets specific symptoms rather than completely altering the streaming architecture, application developers must implement defense-in-depth strategies. Below is a ranked list of practical defenses, ordered by real-world impact.
1. Strict Input Validation on Server Actions (Zod / Valibot)
Because the Flight deserializer processes raw, unvalidated network data before application code ever runs, schema validation must occur at the absolute entry point of every Server Action.
Validation must happen prior to logging, debugging, or data extraction to prevent secondary vulnerabilities like CVE-2025-55183 (source code exposure via stringification):
"use server"
import z from "zod"
const ProfileSchema = z.object(
name: z.string().min(1).max(100),
email: z.string().email(),
)
export async function updateProfile(data: unknown)
// Always validate the raw argument structure directly
const parsed = ProfileSchema.safeParse(data)
if (!parsed.success)
return error: "Invalid input shape"
// Proceed exclusively with validated data
await db.users.update(parsed.data)
2. Enforcing the server-only Package
To prevent sensitive modules containing database connectors, internal business logic, or private keys from accidentally crossing the server-client boundary, import "server-only" at the top of designated files.
Developers must remain cautious with barrel files (e.g., index.ts re-exports), ensuring server-side utilities are never bundled alongside client-safe components.
3. Comprehensive CSRF Hardening
Relying solely on default framework checks for state-changing Server Actions is insufficient (as demonstrated by CVE-2026-27978). Developers should enforce explicit session cookies (SameSite=Strict), implement per-session CSRF tokens for high-value actions, and ensure that configuration flags like experimental.serverActions.allowedOrigins never include 'null'.
4. Dependency Auditing and Patch Management
Verify that your project lockfiles do not run vulnerable framework versions. Ensure dependencies sit at or above 19.0.4+, 19.1.5+, or 19.2.4+ to protect against both RCE vectors and subsequent Denial-of-Service and memory exhaustion patches.
5. Leveraging the React Taint API
Use taintObjectReference and taintUniqueValue during development to throw runtime errors if sensitive objects (such as full user models or auth tokens) are accidentally passed as props to Client Components:
import experimental_taintObjectReference as taintObjectReference from "react"
export async function fetchUserData(id: string)
const user = await db.query(id)
taintObjectReference("Do not pass raw user records to client components.", user)
return user
Note: Taint tracking relies strictly on object references and is bypassed by structural cloning or data spreading; treat it as a development-time guardrail rather than an airtight security boundary.
6. Web Application Firewall (WAF) Rule Tuning
Deploy custom WAF signatures to inspect incoming requests carrying Next-Action headers. Block request bodies containing prototype pollution strings (__proto__, constructor:constructor) and set payload size limits to mitigate zipbomb-style decompression vectors.
Future Outlook
The introduction of React Server Components and the Flight protocol represents a monumental shift in how full-stack JavaScript applications are built, solving complex challenges around streaming component trees and progressive hydration. However, history demonstrates that whenever frameworks introduce custom wire formats to move rich, stateful, and executable data across the network, security challenges inevitably follow.
As more modern frameworks adopt server-driven UI paradigms, the industry will inevitably demand more resilient foundational primitives. Moving beyond the assumption that "the server is trusted," future protocol iterations will likely require cryptographic verification of serialized payloads, signed component trees, and strict content integrity checks directly on the stream itself.
Until then, engineers deploying React applications must look beyond default framework protections, maintaining rigorous input validation, strict boundary isolation, and proactive patch management.
