A critical Remote Code Execution (RCE) vulnerability — CVE‑2025‑55182 — recently shook the React ecosystem. It targeted React Server Components (RSC) and React Server Functions, exposing how a subtle deserialization bug could lead to arbitrary code execution on the server.
- Affected Versions
- Why This Matters
- A New Rendering Paradigm
- What Is the React Flight Protocol?
- Why React Server Components Exist
- Where Things Went Wrong
- Prototype Pollution as the Root Cause
- From Pollution to RCE
- The Real‑World Exploit Path
- The Fix: What Changed in PR #35277
- Mitigation and What You Should Do
- Final Thoughts
The vulnerability affects React 19 Server Component infrastructure prior to the December 2025 security patch.
react@19.0.0react@19.1.0react@19.1.1react@19.2.0
react-server-dom-webpackreact-server-dom-turbopackreact-server-dom-parcel
Upgrade to one of the following:
19.0.119.1.219.2.1
Frameworks such as Next.js that bundle RSC functionality released coordinated fixes. If you are using Server Actions, upgrading React alone is not sufficient — the framework must also be patched.
This vulnerability is unauthenticated and pre‑render. An attacker does not need:
- User credentials
- A valid session
- JavaScript execution in the browser
A single crafted HTTP request to a Server Function endpoint is enough.
This places CVE‑2025‑55182 in the highest severity category for production applications.
Historically, React applications lived in one of two worlds:
- Client-Side Rendering (CSR) — logic runs entirely in the browser
- Server-Side Rendering (SSR) — the server renders HTML for each request
Both models have trade‑offs. CSR shifts work to the client but delays meaningful content. SSR improves first paint but still ships all component logic to the browser.
React Server Components (RSC) introduce a fundamentally different idea:
Not all components need to exist in the browser.
With RSC, some components run only on the server. They can:
- Access databases directly
- Read from the filesystem
- Use secrets and credentials
- Execute heavy computation
…and none of that code is sent to the client.
Instead of HTML, the server returns a description of the component tree using a compact binary protocol. The browser receives instructions — not markup — and stitches together the final UI.
This model unlocks performance and architectural benefits that neither CSR nor SSR could achieve alone.
The React Flight Protocol (RFP) is the transport layer that makes Server Components possible.
It is not JSON. It is a streaming, chunk-based serialization format.
Chunk 0 → $1
Chunk 1 → { "message": "$2" }
Chunk 2 → "Hello"<ServerComponent message="Hello" />Instead of HTML, the server sends instructions:
- Render this component
- With these props
- Insert client boundaries here
On the client, React reconstructs the tree:
const element = createElement(ServerComponent, { message: "Hello" })This reconstruction step is where deserialization occurs — and where security boundaries matter.
Before diving into the vulnerability, it helps to understand why RSC exists at all.
Server Components never ship to the browser.
// Server Component (no "use client")
import { db } from '@/db'
export async function UserProfile({ id }) {
const user = await db.users.find(id)
return <div>{user.name}</div>
}The above component:
- Runs only on the server
- Can access databases and secrets
- Adds zero bytes to the client bundle
Only Client Components — explicitly marked — are bundled for the browser:
"use client"
import { useState } from 'react'
export function LikeButton() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}Traditional SSR requires API routes or data-fetching abstractions.
// Old pattern
fetch('/api/user?id=123')With RSC, data lives where it’s used:
// Server Component
const user = await db.users.find(123)No REST. No GraphQL. No serialization hop.
RSC flips the default assumption:
Old: UI → API → DB
New: UI (server) → DBThe server becomes the primary execution environment. The client is for interactivity only.
Server Components stream incrementally:
import { Suspense } from 'react'
export default async function Page() {
return (
<>
<Header />
<Suspense fallback={<Spinner />}>
<SlowContent />
</Suspense>
</>
)
}The browser receives UI as soon as each piece is ready.
During deserialization, React resolves paths like:
$1:propertyA:propertyB
If propertyA does not exist, JavaScript’s normal behavior applies:
Walk up the prototype chain.
This is usually harmless — until user‑controlled input can influence the path.
During deserialization, React resolves reference paths like:
$1:propertyA:propertyBIf propertyA does not exist, JavaScript walks the prototype chain.
The __proto__ property (or [[Prototype]] internal slot) in JavaScript is a reference to the prototype object of an object. When you try to access a property on an object, and that property isn't directly on the object itself, JavaScript will automatically look up the __proto__ chain to find it on the prototype object, then its prototype, and so on, until it reaches null. This mechanism allows objects to inherit properties and methods from other objects.
Object
→ Object.__proto__
→ Object.__proto__.constructor
→ Object.__proto__.constructor.constructorWhich resolves to:
FunctionThis happens because:
({}).constructor.constructor === FunctionOnce an attacker gains access to Function, arbitrary code execution becomes possible.
Access to the Function constructor enables runtime code execution.
const fn = Function("return process")()
fn.mainModule
.require('child_process')
.execSync('id')The exploit injected Function into an internal call path that React expects to invoke:
response._formData.get(response._prefix + "0")After prototype pollution, this effectively became:
Function(attackerControlledString)At that point, deserialization turns into execution.
The attack arrives as a multipart/form-data request that looks like a legitimate Server Function call.
Key traits:
- Crafted Flight chunks
- Fake promise‑like objects
- Self‑referential chunk loops
React attempts to resolve the data — and executes attacker‑controlled code instead.
The core fix is deceptively small.
Before:
moduleExports[name]After:
if (hasOwnProperty.call(moduleExports, name)) {
return moduleExports[name];
}
return undefined;- Prevents prototype chain traversal
- Blocks
__proto__,constructor, and inherited access - Turns dangerous paths into harmless
undefined
This change was applied consistently across all Server DOM implementations.
- Upgrade React to a patched version
- Upgrade your framework (Next.js, Remix, etc.)
- Audit any custom Server Function handlers
- Treat deserialization as untrusted input
If you cannot upgrade immediately, disable Server Functions as a temporary mitigation.
This vulnerability wasn’t caused by unsafe code execution APIs — it emerged from perfectly valid JavaScript behavior interacting with a powerful serialization format.
As React moves deeper into server‑first architectures, security boundaries matter more than ever. Serialization is code execution adjacent — and must be treated with the same caution.
The fix in PR #35277 closes this chapter, but the lesson remains: facebook/react#35277
Complexity is where security bugs hide.
Stay patched. Stay curious.