Skip to content

Instantly share code, notes, and snippets.

@sriram-palanisamy-hat
Last active December 18, 2025 01:25
Show Gist options
  • Select an option

  • Save sriram-palanisamy-hat/d207174ada2fa052ad44439f22a65c7e to your computer and use it in GitHub Desktop.

Select an option

Save sriram-palanisamy-hat/d207174ada2fa052ad44439f22a65c7e to your computer and use it in GitHub Desktop.

The Invisible Handshake: Understanding the RCE Flaw in React Server Functions

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.


Table of Contents


Affected Versions

The vulnerability affects React 19 Server Component infrastructure prior to the December 2025 security patch.

Vulnerable React Versions

  • react@19.0.0
  • react@19.1.0
  • react@19.1.1
  • react@19.2.0

Affected Packages

  • react-server-dom-webpack
  • react-server-dom-turbopack
  • react-server-dom-parcel

Fixed Versions

Upgrade to one of the following:

  • 19.0.1
  • 19.1.2
  • 19.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.


Why This Matters

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.


A New Rendering Paradigm

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.


What Is the React Flight Protocol?

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.

Example: Logical Representation

Chunk 0 → $1
Chunk 1 → { "message": "$2" }
Chunk 2 → "Hello"

Example: What React Is Describing

<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.


Why React Server Components Exist

Before diving into the vulnerability, it helps to understand why RSC exists at all.

1. Smaller JavaScript Bundles

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>
}

2. Direct Data Access (No API Layer)

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.


3. Server-First Mental Model

RSC flips the default assumption:

Old: UI → API → DB
New: UI (server) → DB

The server becomes the primary execution environment. The client is for interactivity only.


4. Progressive Streaming

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.


Where Things Went Wrong

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.


Prototype Pollution as the Root Cause

During deserialization, React resolves reference paths like:

$1:propertyA:propertyB

If 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.

Malicious Traversal

Object
→ Object.__proto__
→ Object.__proto__.constructor
→ Object.__proto__.constructor.constructor

Which resolves to:

Function

This happens because:

({}).constructor.constructor === Function

Once an attacker gains access to Function, arbitrary code execution becomes possible.


From Pollution to RCE

Access to the Function constructor enables runtime code execution.

Execution Primitive

const fn = Function("return process")()
fn.mainModule
  .require('child_process')
  .execSync('id')

How React Was Tricked

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 Real‑World Exploit Path

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 Fix: What Changed in PR #35277

The core fix is deceptively small.

Before:

moduleExports[name]

After:

if (hasOwnProperty.call(moduleExports, name)) {
  return moduleExports[name];
}
return undefined;

Why This Matters

  • 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.


Mitigation and What You Should Do

  1. Upgrade React to a patched version
  2. Upgrade your framework (Next.js, Remix, etc.)
  3. Audit any custom Server Function handlers
  4. Treat deserialization as untrusted input

If you cannot upgrade immediately, disable Server Functions as a temporary mitigation.


Final Thoughts

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment