How React Server Components stream behavior, not just data — and why CVE-2025-55182 (React2Shell) turned that design into a CVSS 10.0 RCE. Practical defenses inside.
React Server Components (RSCs) have moved from experimental to load-bearing in the space of two years. Next.js's App Router, and comparable approaches in other frameworks, now ship UI that is streamed from server to client as more than plain data — it carries behavior. Underneath that experience sits a protocol most developers never touch directly but implicitly trust on every request: React Flight.
Flight looks JSON-adjacent but isn't JSON. It's a purpose-built, line-delimited streaming format with its own type system and its own reference-resolution logic. This piece walks through how Flight actually works, why that design creates a structural attack surface, and what concrete steps teams should take in light of CVE-2025-55182, publicly nicknamed "React2Shell" — one of the more severe frontend security events of the last few years.
What Is Flight Actually Streaming?
Flight ships data line by line, each line following the pattern <ROW_ID>:<ROW_TAG><PAYLOAD>. The row tag tells the client runtime which execution path to take:
- J (JSON Tree): the virtual DOM tree and props produced by a server component.
- I (Import): which client-side module the browser needs to load.
- M (Module metadata): supporting metadata for that import.
- F (Server Reference): a callable reference into a Server Action / RPC endpoint.
- L (Lazy Component): a component reference resolved later.
- @ (Raw Chunk): direct access to the framework's internal, mutable stream state.
- B (Blob): rows carrying binary payloads.
The important part is this: Flight doesn't just reconstruct data, it reconstructs executable behavior. References prefixed with $ route into different execution paths — $ resolves a model reference, $: walks property access (including the prototype chain), $F materializes a callable RPC endpoint into a Server Function, $@ exposes raw internal Chunk objects (mutable framework state), and $B triggers a Blob handler. And critically, all of this deserialization happens before your application-level input validation ever runs. Untrusted network input is interpreted — and partially executed — at the protocol layer before your code gets a chance to ask "is this safe?"
That's a structurally different risk model from JSON.parse() plus manual validation. With JSON, deserialization produces data. With Flight, deserialization can produce behavior.
React2Shell: From a Missing hasOwnProperty Check to Full RCE
CVE-2025-55182 is an unauthenticated remote code execution vulnerability in the Flight deserialization layer, rated CVSS 10.0 and widely referred to in the security community as "React2Shell." This isn't a claim resting solely on the original coverage — Google Cloud Threat Intelligence, Rapid7, Zscaler ThreatLabz, Trend Micro, Qualys, Microsoft Security, Datadog Security Labs, Palo Alto Unit42, and Cloudflare have each published their own independent analyses using the same name and the same severity rating.
The root cause was a missing hasOwnProperty guard inside getOutlinedModel, the function responsible for resolving property paths during deserialization. Simplified, the vulnerable loop looked like this:
for (key = 1; key < reference.length; key++) {
parentObject = parentObject[reference[key]];
}
Nothing stopped reference from containing prototype-chain keys like __proto__ or constructor. The exploit chain, roughly:
- An attacker sends a payload containing the path
$1:__proto__:constructor:constructor. - The parser walks from a plain object to
Object.prototype, then to theObjectconstructor, then to theFunctionconstructor. Functionbehaves as JavaScript'sevalequivalent.$@0is used to reach a raw internal Chunk object — mutable framework state.- The Chunk's
.thenproperty is hijacked to intercept React's own Promise resolution. - On a second deserialization pass,
_response._formData.getand_response._prefixare overwritten. $B0(the Blob handler) is triggered, executingFunction("attacker_command")().
Net effect: full shell access from a single HTTP POST request. Within hours of disclosure, North Korean state-linked actors were reported deploying EtherRAT implants that used the Ethereum blockchain as a command-and-control channel — evidence that this wasn't a theoretical bug, it was weaponized in near real time.
Why it's this severe: React2Shell requires no authentication, triggers on a single request, and potentially affects any RSC-based application regardless of framework. A CVSS 10.0 score reflects the combination of trivial exploitability and maximum impact.
Defense: What Actually Moves the Needle
The table below orders the available mitigations by practical impact.
| Mitigation | Impact | Note |
|---|---|---|
| Schema validation on Server Actions (Zod/Valibot) | Critical | Validate raw arguments BEFORE destructuring them |
The server-only package |
High | Protects code, not return values — filter sensitive fields manually |
| CSRF hardening (SameSite, per-session token) | High | Never add 'null' to allowedOrigins |
| Keeping React up to date | Essential | RCE fix shipped in 19.0.1+/19.1.2+/19.2.1+ |
React's Taint API (taintObjectReference) |
Limited | Tracks object references, not the data itself |
| WAF rules | Secondary | Can be bypassed via padding/chunked encoding |
The single highest-leverage change is putting schema validation at the very top of every Server Action — before any business logic, and before destructuring the raw argument. server-only prevents Client Components from importing sensitive code paths at build time, but it does not sanitize what a function returns; filtering sensitive fields out of return values is still on you. On the CSRF side, because sandboxed iframes send Origin: null, adding 'null' to allowedOrigins re-opens the exact bypass tracked as CVE-2026-27978 — a trap worth calling out explicitly.
React2Shell wasn't alone. The same deserialization layer produced a cluster of related issues: CVE-2025-55184 / CVE-2025-67779 (infinite Promise recursion enabling denial of service), CVE-2026-23864 (unbounded request buffering — a zip-bomb-style memory exhaustion vector), and CVE-2025-55183 (source code disclosure via stringified Server Function arguments). All of them share the same underlying pattern: untrusted network input gets deserialized before application-level validation has a chance to run.
The Takeaway for Any Team Shipping RSCs
React2Shell is a strong counter-example to the assumption that a newer, more elegant framework feature is inherently safer. RSCs and the Flight protocol deliver a genuinely better developer experience, but that experience was built by pushing deserialization deeper into the framework, outside the boundary application code typically guards. "Validate your input" is no longer a rule that only applies at your API layer — it now has to apply at the very first line of every Server Action.
The practical shortlist: keep React and your framework (Next.js, Remix, etc.) current, put schema validation at the entry point of every Server Action, mark sensitive code paths with server-only, and re-audit your CSRF/origin configuration. Those four steps meaningfully contain the blast radius of a React2Shell-class vulnerability.


