Lessons learned from React's RCE

On December 14, 2025 by Sosthène Guédon

In the last few weeks, 3 vulnerabilities where found in the React web application framework. The first one, a server-side remote code execution (CVE-2025-55182) is the worst a vulnerability can get for a web framework. The two other ones are a denial of service (CVE-2025-67779), and a source code exposure (CVE-2025-55183), much less dangerous, yet still impactful.

There are already a lot of writeups published talking about how these vulnerabilities happened, but I didn't see much about being said about preventative measures that would have limited the damage.

In this entry, I'll explore potential mitigations that could have been applied ahead of time to prevent these vulnerabilities or at least limit their severity.

The vulnerabilities

The three vulnerabilities exploit React's flight protocol. This protocol is used by React to exchange data between the backend and the frontend, when using Server Components or Server Actions.

The RCE (CVE-2025-55182)

The first vulnerability is the most dangerous. It achieves remote code execution on the server by exploiting multiple features of the React flight protocol:

  • Promises can be serialized
  • Promises in JavaScript are just objects with a .then method
  • Chunks can contain elements that point to properties in other chunks
  • Javascript objects have a __proto__ property that allows accessing the object's prototype, including their constructor
  • The constructor of a function is the function constructor, which can take a string of JavaScript and turn it into a callable function.

In short, it exploits the following facts:

let a = {then: () => console.log("HERE") };
await a; // Prints HERE to the console
function someFunction() {}
let f = someFunction.__proto__.constructor('console.log("HERE2")');
f() // Prints HERE2 on the console
let b = {};
let b_constructor = b["__proto__"]["constructor"] // is a function, so:
b_constructor["constructor"]('console.log("HERE3")')() // Prints HERE3 on the console

Assembled, all these pieces lead to something like:

let c = {}
let d = {
  then: c["__proto__"]["constructor"]["constructor"]('console.log("HERE4")')
};
await d; // Prints HERE4 on the console

This works by making d a chunk that has a then field that refers to the ["__proto__"]["constructor"]["constructor"] properties of an empty chunk c. There are some other subtleties to the vulnerability, but that is the general idea. If you're looking for a more detailed explanation of the protocol and the vulnerability, there are many you can find online, and this repository contains the original proof of concept.

The Denial of Service (CVE-2025-67779)

Since chunks can refer to other chunks, deserializing a flight protocol request were two chunks A and B that reffered to each other threw the parser in an infinite loop. Thus preventing the server from dealing with any other incoming request.

The source code exposure (CVE-2025-55183)

Chunks can refer to other chunks, but not only that. They can also refer to server actions. Javascript's default toString for function displays the function's source code:

function someFunction() {
  1 + 1;
}

console.log(someFunction.toString())
// Displays:
// function someFunction() {
// 1 + 1;
// }

Assuming this would allow an attacker to send a chunk that, when interpreted as a string, would contain any of the application's server action's source code. If this input is then displayed to the user, it could be used for source code exposure.

Problems with the flight protocol

I'm not a JS or React developer and I don't understand all the problems that the flight protocol is designed to solve, so I will refrain from being too critical of it. However, it does seem to me that it's designed to serialize and deserialize pretty much anything. The fact that chunks can refer to fuctions and allows serializing a closure's captures raises concern to me.

It seems to me that instead of serializing "data", the flight protocol tries to serialize "objects", including their behaviour (for example, being a promise, or the captured state of a closure). This is inherently dangerous, treading the same path as PHP's dangerous unserialize and Python's yaml.load. The prevention of future vulnerabilities in the flight protocol will require enumerating badness, which is doomed to face many security holes before all the ways Javascript's dynamism can be exploited are patched.

I feel like the flight protocol should be more strongly typed but I don't know how it could be done better given that JavaScript doesn't have any form of runtime type reflection or compile-time metaprogramming.

Node.js mitigations

Disabling code generation

The first thing that came to mind was that on the frontend we always make use of a Content Security Policy (CSP) to avoid XSS, why is it not the case on the backend? Node.js does not support something equivalent to CSPs, but it does have a flag that disables eval and the Function constructor, and prevents setTimeout and setInterval from running with a string argument (their default behaviour is to execute the string as JavaScript).

This flag is --disallow-code-generation-from-strings.

Running node with this flag configured would have prevented the RCE, which is the worst vulnerability. I did not find a compelling reason why this flag is not the norm for Node.js deployments. It does not appear to affect the Worker or WebAssembly functionality, so I don't even think it would break much code.

It's not a silver bullet, there are still other ways in node to run arbitrary code (child processes, the node:vm module), but it's not clear to me that the prototype tricks the RCE used could give the attacker access to them.

Disabling the prototype

The root of cause of the RCE here, and to many other vulnerabilities is the access to __proto__. Node.js has a flag to remove this property: --disable-proto=delete. Once again, this does not fully remove the risk of prototype pollution, but it makes it harder to reach.

The OWASP cheatsheet on prototype pollution mentions this flag, but I don't see any documentation on the possible impact of this. The existence of this issue and the amount of results (469k) for a search on github for "__proto__ language:JavaScript" suggest it could break some code.

Freezing the prototypes

The OWASP cheatsheet on prototype pollution mentions using Object.freeze() and Object.seal() to prevent prototype pollution. Node.js also has an experimental flag to do something similar: --frozen-intrinsics. As it is experimental, it probably should not be used for now, but it should be kept in mind.

Why I wrote this

I haven't seen any reporting on this vulnerability talk about these Node.js flags, and I don't see them being recommended to be enabled by default. The security best practices page of Node.js mentions --disable-proto=delete and the experimental --frozen-intrinsics but not --disallow-code-generation-from-stringsThe OWASP page on NodeJS doesn't mention any of these flags.

I'm not a JavaScript expert, but these flags seem like they would be pretty significant and straightforward security improvements. Specifically --disallow-code-generation-from-strings appears to be a very obvious setting that would only very rarely break legitimate code, and it should be compatible with a lot of code, given that Cloudflare workers are even stricter than it.

As such, this entry documents these flags and maybe someone will explain to me why they're not used in practice.