Weaponizing and Defending the React Flight Protocol

While React Server Components (RSCs) rely on the custom Flight protocol to stream interactive user interfaces, this same mechanism introduces powerful deserialization vulnerabilities that attackers can exploit. Durgesh Pawar breaks down the mechanics behind the CVSS 10.0 "React2Shell" vulnerability, demonstrating how protocol manipulation can lead to remote code execution. The article also presents a practical, ranked set of defenses, from strict schema validation to CSRF hardening, for securing React applications against these inherent structural risks.
React Server Components do not transmit HTML or JSON to your browser. Instead, when a server component renders, a custom streaming protocol called Flight is transmitted over the wire. This protocol utilizes a line-delimited format with its own type system, reference resolution, and rules for reconstructing executable behavior on the client-side. Many React developers remain unaware of the intricacies of the Flight payload, as the React runtime silently handles its reconstruction into a live component tree. This implicit trust in the framework’s internal mechanisms has been revealed to have significant security implications.
The security community’s attention was sharply drawn to the Flight protocol following the disclosure of CVE-2025-55182 in December 2025, widely dubbed "React2Shell." This vulnerability, rated with a CVSS score of 10.0, represented an unauthenticated remote code execution flaw within the Flight deserialization layer. A single, meticulously crafted HTTP request to a Server Function endpoint was sufficient for an attacker to gain shell access to a vulnerable system, bypassing authentication entirely. The severity of this exploit led the U.S. Cybersecurity and Infrastructure Agency (CISA) to add it to its Known Exploited Vulnerabilities catalog. Further analysis by Sysdig linked in-the-wild exploitation to North Korean state-sponsored actors who deployed file-less implants leveraging the Ethereum blockchain, underscoring the sophisticated nature of the attacks.
Investigating the source code, particularly the getOutlinedModel and getChunk functions where the core resolution logic resides, revealed that React2Shell was not an isolated parsing bug but rather a symptom of a broader deserialization system. Flight’s ability to reconstruct executable references, lazily load components, establish server RPC endpoints, and manage asynchronous state from a stream of text fundamentally positions it as a deserialization system, presenting a substantial attack surface beyond simple coding errors.
This article delves into the inner workings of the Flight protocol, identifying its deserialization sinks, detailing the methods already weaponized by attackers, and exploring potential future threats. It culminates in a ranked, practical guide to defenses for developers building with Server Components, including rigorous schema validation for Server Actions, the strategic use of the server-only package, enhanced Cross-Site Request Forgery (CSRF) hardening, and an assessment of the contributions of the Taint API and Web Application Firewalls (WAFs).
Flight On The Wire
Developers can observe the Flight protocol in action by examining the Network tab in their browser’s developer tools when loading a Next.js App Router page. Requests returning Content-Type: text/x-component are transmitting Flight data. This data is not a monolithic JSON blob but a streaming, line-delimited format where each line, or "row," is processed by the client-side React runtime as it arrives.
A basic Flight payload illustrates its distinct 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 serves as an import directive, instructing the client to load ClientComponent.js from the bundler’s chunk map. Row 2 represents a JSON tree that constructs an <article> HTML element, with "$1" within children referencing chunk 1 (the imported component). Row 0 defines the server execution context, identifying it as a RootLayout operating in the Server environment. This simple example highlights Flight’s unique combination of structural data, module references, and inter-chunk pointers, distinguishing it from standard JSON.
The Row Format
Each row in a Flight payload adheres to a consistent syntax: <ROW_ID>:<ROW_TAG><PAYLOAD>n. The ROW_ID is a numerical identifier for cross-referencing. The ROW_TAG is a single character or short string indicating the data type, and the PAYLOAD contains the actual content.
The identified row tags include:
- J (JSON Tree): Serialized virtual DOM nodes, component props, and HTML elements.
- M (Module): Metadata for client component modules or chunks.
- I (Import): Instructs the client to load a module from the bundler’s chunk map.
- HL (Hint/Preload): Directs the browser to preload resources like stylesheets or fonts.
- D (Data): Server-rendered element context and environment information.
- E (Error): Serialized server-side exceptions and error boundary data.
While these tags define a structured data format, the true complexity and attack surface lie within the "prefix system."
The $ Prefix System
The $ prefix signifies a departure from literal string interpretation. When encountered by the client-side parser, it triggers a specialized resolution path based on the character immediately following the $. The parseModelString function in ReactFlightClient.js acts as a dispatcher for these prefixed strings:
$(Model Reference): Resolves to another chunk within the stream (e.g.,$2refers to row 2).$:(Property Access): Traverses properties within a resolved chunk (e.g.,$1:user:nameaccessesnameon theuserproperty of chunk 1). This mechanism is akin to prototype pollution vulnerabilities in JavaScript.$S(Symbol): Creates a native JavaScriptSymbol.$F(Server Reference): Represents a callable Server Action, functioning as an RPC endpoint on the server.$L(Lazy Component): Defers the loading of a component until it is required in the render tree.$@(Promise/Raw Chunk): Returns the internalChunkwrapper object, which React uses to manage resolution state, pending callbacks, and metadata. This is crucial for Promise handling and exploited by attackers to gain mutable handles. The exposure of internal framework plumbing through the protocol is a significant design concern.$B(Blob/Binary): Invokes the blob deserialization handler for binary data.
While most prefixes resolve to parsed data, $@ provides the raw Chunk object, exposing internal framework mechanics. The $: prefix, enabling arbitrary property traversal via colon-separated paths, is particularly concerning due to its resemblance to prototype pollution attack vectors.
This Is Not Just A Data Format
Flight transcends being a mere data format; it serializes behavior. It reconstructs module references that trigger client-side code execution, establishes server action endpoints for client invocation as RPC calls, sets up Promise chains for asynchronous operations, and builds lazy-loaded components that execute on demand. This fundamentally differs from JSON, which exclusively provides data. The Flight stream dictates client runtime actions, including code loading, function invocation, and data trust.
The underlying mechanics bear a striking resemblance to historically problematic deserialization systems. The complexity of chunk resolution in the source code, particularly in react-client/src/ReactFlightClient.js and react-server/src/ReactFlightServer.js, makes static analysis challenging, often necessitating debugging with breakpoints.
Why Flight Is A Deserialization Sink
The deserialization pattern is a well-established source of vulnerabilities across various programming languages. Java’s ObjectInputStream (exploited by ysoserial), Python’s pickle (code execution on load()), PHP’s unserialize (triggering magic methods), and .NET’s BinaryFormatter (leading to its deprecation) all exemplify how deserializing attacker-controlled input can lead to uncontrolled execution.
While JSON.parse() in JavaScript is inherently safe, producing only plain data objects without executing constructors or magic methods, this safety is compromised when a framework wraps custom deserialization logic around it. Flight exemplifies this scenario.
Prototype Pollution
JavaScript’s prototype-based inheritance allows property lookups to traverse the __proto__ chain. Attackers can inject __proto__ or constructor.prototype as keys during deserialization, modifying shared base prototypes. This leads to downstream code unknowingly reading attacker-controlled values. Flight’s $: prefix facilitates this by enabling arbitrary property traversal through getOutlinedModel. If path segments include __proto__ or constructor, the traversal ascends the prototype chain, a direct pathway to prototype pollution, as demonstrated in the React2Shell exploit.
Duck Typing and Thenables
The V8 engine and the JavaScript specification treat any object with a .then property as a "Thenable." When await is used, the runtime checks for and invokes this .then property. Flight’s asynchronous chunk resolution allows attackers to craft objects with manipulated .then properties. If such an object enters the chunk resolution pipeline, the runtime will execute the attacker’s function during normal await operations, leveraging JavaScript’s language semantics for exploitation. While initial focus might be on $F (Server Actions), the $: property traversal and the manipulation of chunk status transitions were also investigated for exploit potential.
The Core Problem
The convergence of prototype pollution and thenable manipulation in Flight stems from the protocol’s fundamental design: it deserializes behavior, not just data. The $ prefix system dictates the parser’s control flow, enabling the creation of callable server endpoints ($F), lazy code loading ($L), blob handling ($B), and exposure of internal framework state ($@). When an attacker can influence the stream’s content, they gain control over the parser’s function calls, object constructions, and exposed internal state.
The Mechanics Of React2Shell
CVE-2025-55182, React2Shell, is a prime example of this vulnerability, achieving CVSS 10.0 for unauthenticated remote code execution within the Flight deserialization layer. A single HTTP request granted attackers full shell access. Understanding the exploit chain illuminates the power granted to attackers who can control the Flight stream.
The Root Cause
The vulnerability resides in getOutlinedModel, responsible for resolving deep property paths via the $: reference system. In the server-side reply handling code (ReactFlightReplyServer.js), the parser splits colon-separated path segments and traverses them sequentially:
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]];
This loop lacks hasOwnProperty checks or validation against prototype chain traversal. By supplying a path like $1:__proto__:constructor:constructor, an attacker can traverse from a plain JSON object up the prototype chain to the Function constructor, which in JavaScript behaves akin to eval(), enabling arbitrary code execution via Function("arbitrary code")(). The absence of allowlists for property names and specific checks for __proto__ left this vector open.
The Gadget Chain

Transitioning from reaching the Function constructor to achieving RCE requires chaining multiple Flight protocol features. The exploit chain, detailed in a Resecurity write-up, involves a sequence where legitimate Flight protocol features are manipulated in unforeseen ways, highlighting how the composition of these features, when attacker-controlled, leads to vulnerabilities.
Impact
The impact of React2Shell was profound:
- CVSS 10.0: Maximum severity rating.
- Unauthenticated RCE: No credentials required for exploitation.
- Widespread Affected Versions: All applications using React Server Components prior to the patch.
What Happened In The Wild
Exploitation was swift. Sysdig reported that state-sponsored actors weaponized the vulnerability within hours of its disclosure, deploying the EtherRAT implant. EtherRAT utilizes the Ethereum blockchain for command-and-control (C2) communication, a technique known as "EtherHiding," making takedown operations exceptionally difficult.
Palo Alto’s Unit 42 documented the KSwapDoor backdoor, which masqueraded as a legitimate Linux kernel process. KSwapDoor employed RC4 encryption for its internal strings and configuration, with C2 communications secured by AES-256-CFB and Diffie-Hellman key exchange over a P2P mesh network. The rapid and sophisticated exploitation by state actors underscores the critical need for immediate patching of CVSS 10.0 vulnerabilities in deserialization layers.
The Fix
The React team’s patch addressed the vulnerability by caching the genuine hasOwnProperty method at module load time:
var hasOwnProperty = Object.prototype.hasOwnProperty;
Subsequent property checks in the deserialization path utilize .call() to invoke this cached reference:
hasOwnProperty.call(value, i);
This effectively blocks prototype chain traversal, even if an attacker attempts to shadow hasOwnProperty on a malicious object. The fix was integrated into React 19.0.1, 19.1.2, and 19.2.1.
While the patch resolves the known exploit chain, the underlying property traversal model remains. The $: prefix still facilitates path traversal, but now with ownership validation. However, exposing arbitrary property traversal via a network protocol is arguably a design flaw, and the patch addresses the symptom rather than the root cause. Future vulnerabilities may emerge from this same area. The framework patch secures against the known gadget chain but does not fundamentally alter the protocol’s nature of reconstructing behavior from a text stream. Reliance solely on the framework for security means trusting that all deserialization parser edge cases have been identified and fixed.
Defenses, Ranked By Impact
A multi-layered defense strategy is crucial for mitigating the risks associated with the Flight protocol. The following defenses are ranked by their impact, based on vulnerability research:
-
Input Validation On Server Actions (Zod, Valibot): This is the most critical application-level defense. The Flight deserializer processes raw network input before application code executes. Strict schema validation at the very beginning of every Server Action prevents malicious data from propagating. Validation should precede any business logic or logging to avoid leaking source code through implicit stringification bugs. Libraries like Zod and Valibot can be used to enforce type, shape, length, and value constraints. It is vital to use
.safeParse()and to validate the entire argument object before destructuring to prevent attacks during property access. -
The
server-onlyPackage: This straightforward package prevents server-side code from being imported into client components. By importing"server-only"at the top of sensitive files (e.g., those containing database credentials or business logic), build-time errors are triggered if a client component attempts a transitive import. Care must be taken with barrel files to ensure server-only modules are not inadvertently exposed. It’s important to note thatserver-onlyprevents code leakage, not data leakage; return values must still be carefully filtered. -
CSRF Protections: Following CVE-2026-27978, relying solely on framework-level CSRF checks can be insufficient. For state-changing Server Actions, implementing additional protections is recommended. This includes configuring session cookies with
SameSite=StrictorSameSite=Laxand, for high-value operations, employing explicit CSRF tokens validated on the server. Critically, theexperimental.serverActions.allowedOriginsconfiguration in Next.js should never include'null', as this reopens theOrigin: nullbypass. -
The
hasOwnPropertyPatch: The fix for React2Shell, which involves cachinghasOwnProperty, is correct and effectively neutralizes the known RCE exploit chain. Verifying that applications are running patched versions of React (19.0.1+, 19.1.2+, 19.2.1+) is essential. Additionally, ensuring updates for related Denial of Service (DoS) vulnerabilities (19.0.4+, 19.1.5+, 19.2.4+) is crucial. This patch is a reactive measure, addressing a specific exploit rather than redesigning the protocol’s fundamental structure. -
The Taint API: React’s Taint API (
taintObjectReference,taintUniqueValue) aims to prevent sensitive data from leaking to the client by throwing errors during serialization if tainted data is passed to client components. While useful as a development guardrail for honest mistakes, it is reference-based and breaks with data transformations or new object creation. It is not a robust security boundary against motivated attackers. -
WAFs: Web Application Firewalls can provide an additional detection layer by inspecting requests for known attack patterns, such as prototype pollution attempts or specific Flight error response formats. However, WAFs can be bypassed through padding or encoding techniques, making them a supplementary defense rather than a primary security boundary.
What Came After React2Shell
The React2Shell disclosure triggered further security audits, uncovering several related vulnerabilities in the Flight deserialization surface. While less severe than the initial RCE, these issues highlight the persistent challenges in securing complex deserialization parsers.
- CVE-2025-55184 & CVE-2025-67779 (DoS): These vulnerabilities involved infinite recursion of nested Promises and edge cases missed by initial fixes, leading to denial-of-service by hanging the Node.js event loop.
- CVE-2026-23864 (DoS/OOM): Disclosed in January 2026, this vulnerability involved unbounded request body buffering and zipbomb-style decompression, leading to memory exhaustion.
- CVE-2025-55183 (Info Disclosure): This bug allowed attackers to reflect Server Function source code by crafting arguments that, when stringified, exposed the function’s source, revealing sensitive business logic and secrets.
- CVE-2026-27978 (CSRF Bypass): A CSRF bypass in Next.js’s Server Action handling allowed exploitation via
Origin: nullrequests originating from sandboxed iframes.
The repeated patching of DoS vulnerabilities underscores the difficulty of thoroughly securing deserialization parsers. CVE-2025-55183, in particular, highlights the risk of accidental data leakage through common developer practices like logging. CVE-2026-27978 points to subtle misinterpretations of HTTP headers in framework-level security mechanisms.
What’s Still Exposed
Beyond the specific CVEs, certain risks are inherent to the Flight protocol’s design:
-
Man-In-The-Middle (MITM) On The Flight Stream: If an attacker can intercept the communication between server and client, they could potentially modify the Flight stream in transit. Altering import directives (
$I), injecting server references ($F), or modifying data rows (D) could lead to component redirection, hidden RPC triggers, or even direct XSS vulnerabilities if components usedangerouslySetInnerHTML. Flight’s string escaping mechanisms protect against data being interpreted as protocol instructions, but a MITM attacker writes raw protocol, bypassing this defense. -
Server Action Enumeration: Server Action IDs, while obfuscated, can be enumerated by accessing the public
server-reference-manifest.json. This exposes a complete API map to attackers, enabling IDOR and parameter tampering attacks. Developers often implicitly trust inputs from React’s internal machinery, overlooking the consequences of this misplaced trust. -
Encrypted Closure Tampering: Server Actions that capture variables from their scope are encrypted for transit. While the encryption key regenerates with each build by default, static keys in multi-server environments can be compromised. File read access could allow attackers to extract the key, decrypt closure state, tamper with it (e.g., alter
userId), and re-encrypt, leading to forged requests accepted by the server. -
Supply Chain Activation via Module IDs: Flight references client components by module ID. A compromised npm package in
node_modules, even if not directly imported, could be present in the bundle output. If an attacker injects$Iimport references into the Flight stream, the parser might load this dormant module, potentially leading to malicious code execution if chunk-level validation is insufficient.
This Has Happened Before
React Flight is not the first framework to encounter security challenges with custom serialization formats. Google Web Toolkit (GWT) faced similar issues with its RPC protocol, eventually disabling binary serialization. Java Server Faces (JSF) and ASP.NET serialized ViewState to the client, leading to remote code execution when cryptographic signing was weak. The recurring pattern involves frameworks creating custom wire formats for rich, stateful data transfer, assuming server trust and client compliance, only to discover that these formats can be manipulated in transit or by tricking the server into deserializing malicious input. React Flight represents the latest iteration of this long-standing security challenge.
Where This Goes Next
The React Flight protocol effectively addresses the complex challenge of streaming interactive component trees from server to client, facilitating progressive hydration, asynchronous data loading, and server-driven code splitting. The React team has implemented critical patches, including the hasOwnProperty fix and remedies for DoS vulnerabilities, which are essential for securing applications.
However, the fundamental design choice of exposing arbitrary property traversal and executable Thenable reconstruction through a network-facing protocol is a significant concern. The $:, $@, and $B prefixes, powerful internal primitives, were reachable through a parser that lacked sufficient ownership validation, leading to the CVSS 10.0 vulnerability.
As more frameworks adopt server-driven UI patterns, the industry will require stronger primitives than simply "the server is trusted." This includes cryptographic validation of serialized payloads, signed component trees, and content integrity checks on the Flight stream itself. The history of deserialization vulnerabilities suggests that relying solely on the parser to handle every edge case is an untenable security strategy. Developers building with Server Components must thoroughly understand the code in react-client/src/ReactFlightClient.js and implement robust, layered defenses to ensure the security and integrity of their applications.







