The Payload You Never Inspected

Open the Network tab on any Next.js App Router page and filter for Content-Type: text/x-component. What you'll see isn't JSON. It's Flight — React's line-delimited streaming protocol with its own type system, its own reference resolution, and its own rules for reconstructing executable behavior on the client.

Most React developers never look. The framework handles it, so nobody questions what that trust implies. Then in December 2025, CVE-2025-55182 dropped — a CVSS 10.0 unauthenticated RCE in the Flight deserialization layer. CISA added it to the Known Exploited Vulnerabilities catalog. Sysdig tied in-the-wild exploitation to DPRK state-sponsored actors deploying file-less implants via the Ethereum blockchain.

This isn't a one-off parsing bug. It's a symptom. Flight reconstructs executable references, lazy-loaded components, server RPC endpoints, and async state from a stream of text. That's a deserialization system — and it inherits every classic deserialization risk.

Security researcher analyzing React Flight protocol payload in browser network tab for deserialization vulnerabilities Developer Related Image

Flight On The Wire: The $ Prefix System

Every Flight row follows the syntax ID:TAG:PAYLOAD. The interesting part is the $ prefix system — when the client parser encounters a string starting with $, it doesn't treat it as literal text. It routes it through a type-specific resolution path in parseModelString.

PrefixTypeParser Behavior
$Model ReferenceResolves to another chunk in the stream
$:Property AccessTraverses into a resolved chunk's properties ($1:user:name)
$FServer ReferenceRepresents a callable Server Action (RPC endpoint)
$LLazy ComponentDefers component loading until needed
$@Raw ChunkReturns the internal Chunk wrapper (Thenable)
$BBlob/BinaryTriggers blob deserialization handler

$: is the critical one for exploitation. It lets the protocol specify arbitrary property traversal driven by data in the stream. If you've audited JavaScript for prototype pollution, that pattern should feel familiar.

The React2Shell Gadget Chain

The root cause sat in getOutlinedModel, the function resolving deep property paths from $: references:

// Vulnerable loop — no hasOwnProperty check
for (key = 1; key < reference.length; key++)
  parentObject = parentObject[reference[key]];

An attacker supplies $1:__proto__:constructor:constructor, and the loop walks from a plain JSON object up through Object.prototype to the Function constructor. Function("arbitrary code")() executes. No allowlist, no __proto__ check.

From there, the chain composes legitimate Flight features:

  1. Prototype walk to Function via $: path traversal
  2. Raw chunk self-reference via $@0 to get a mutable internal handle
  3. Thenable hijack — set .then to Chunk.prototype.then so React awaits the manipulated chunk
  4. Context confusion — overwrite _response._formData.get to point to Function constructor
  5. Trigger via $B0 — blob handler calls Function("attacker_command")()

Each step is a legitimate feature. The vulnerability emerges from composition.

Node.js server processing streaming React Server Component chunks with prototype pollution attack vectors highlighted Dev Environment Setup

Defenses, Ranked By Impact

1. Schema Validation on Every Server Action

The single most impactful application-level defense. Validate with Zod or Valibot at the very top of every Server Action — before logging, before any business logic. Use .safeParse(), not .parse().

"use server"
import { z } from "zod"

const CommentSchema = z.object({
  postId: z.string().uuid(),
  body: z.string().min(1).max(5000),
})

// Good: validate the raw argument first
export async function addComment(data) {
  const parsed = CommentSchema.safeParse(data)
  if (!parsed.success) return { error: "Invalid input" }
  await db.comments.create(parsed.data)
}

If your Server Action doesn't start with a schema parse, treat it as a vulnerability. Consider a custom ESLint rule flagging any "use server" export without a validation call in its first statement.

2. The server-only Package

Import server-only at the top of any file with DB credentials or internal logic. Build fails if a Client Component imports it transitively. Watch out for barrel files — they silently break the boundary.

3. CSRF Hardening Beyond Framework Defaults

CVE-2026-27978 showed Next.js's Origin vs Host check has edge cases (Origin: null bypass from sandboxed iframes). Set SameSite=Strict on session cookies, add per-session CSRF tokens for high-value operations, and never add "null" to allowedOrigins.

4. Verify Your Patch Version

The RCE fix landed in React 19.0.1, 19.1.2, 19.2.1. DoS fixes need 19.0.4+, 19.1.5+, or 19.2.4+. Check your lockfile:

npm ls react react-dom react-server-dom-webpack

5. The Taint API — Dev Guardrail, Not Security Boundary

taintObjectReference catches honest mistakes (passing a full user object to a Client Component). But taint tracks references, not data content — any spread, derivation, or serialization round-trip breaks it. Useful defense-in-depth, not a primary boundary.

6. WAFs — Noise Reduction, Not a Boundary

WAFs catch automated scanners. They don't stop motivated attackers: inspection buffers are ~128KB, so prepend 130KB of padding and the payload slips through. Treat WAF coverage as noise reduction, not security.

What's Still Structurally Exposed

  • MITM on the Flight stream — plain-text, predictable structure. Inject $I imports, $F server references, or modify D data rows.
  • Server Action enumeration — exposed server-reference-manifest.json hands attackers a complete API map for IDOR attacks.
  • Encrypted closure tampering — static NEXT_SERVER_ACTIONS_ENCRYPTION_KEY in multi-server setups + file read = forged closures.
  • Supply chain activation — dormant npm packages bundled but never loaded can be activated via injected $I references.

Developer reviewing ReactFlightClient.js source code to understand Flight protocol deserialization sinks Programming Illustration

Where This Goes Next

The Flight protocol solves a genuinely hard problem: streaming interactive component trees with progressive hydration and server-driven code splitting. It works. But it works by serializing executable references over a text protocol and trusting the stream's structure on both ends.

The hasOwnProperty patch is correct. The DoS fixes are in place. But exposing arbitrary property traversal and executable Thenable reconstruction through a network-facing protocol was a design mistake. The patch treats the symptom.

Hoping the parser handles every edge case hasn't worked historically — GWT, JSF ViewState, ASP.NET ViewState all followed the same pattern. If you ship Server Components, read react-client/src/ReactFlightClient.js. Know what your framework trusts on your behalf.

Further reading:

Source: Smashing Magazine — Weaponizing and Defending the React Flight Protocol

This content was drafted using AI tools based on reliable sources, and has been reviewed by our editorial team before publication. It is not intended to replace professional advice.