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.

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.
| Prefix | Type | Parser Behavior |
|---|---|---|
$ | Model Reference | Resolves to another chunk in the stream |
$: | Property Access | Traverses into a resolved chunk's properties ($1:user:name) |
$F | Server Reference | Represents a callable Server Action (RPC endpoint) |
$L | Lazy Component | Defers component loading until needed |
$@ | Raw Chunk | Returns the internal Chunk wrapper (Thenable) |
$B | Blob/Binary | Triggers 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:
- Prototype walk to Function via
$:path traversal - Raw chunk self-reference via
$@0to get a mutable internal handle - Thenable hijack — set
.thentoChunk.prototype.thenso React awaits the manipulated chunk - Context confusion — overwrite
_response._formData.getto point toFunctionconstructor - Trigger via
$B0— blob handler callsFunction("attacker_command")()
Each step is a legitimate feature. The vulnerability emerges from composition.

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
$Iimports,$Fserver references, or modifyDdata rows. - Server Action enumeration — exposed
server-reference-manifest.jsonhands attackers a complete API map for IDOR attacks. - Encrypted closure tampering — static
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYin multi-server setups + file read = forged closures. - Supply chain activation — dormant npm packages bundled but never loaded can be activated via injected
$Ireferences.

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:
- Cloudflare DDoS Report H1 2026: 1 Tbps Attacks Surge 519% — What It Means for Your Infrastructure
- From Oracle to PostgreSQL on Azure: A Practical Enterprise Migration Blueprint
Source: Smashing Magazine — Weaponizing and Defending the React Flight Protocol