🚀 Join our AI Wargame at Black Hat Asia and our Workshop + Wargame at NDC Sydney .


React2Shell (CVE-2025-55182): Exploitation Flow and Secure Coding Lessons

19/05/2026

We already discussed the impact of React2Shell in our previous blog post and also created an Incident Response challenge that shows how the exploitation of this vulnerability can be detected by analyzing log files. The challenge also comes with an exploitation POC triggering a Remote Code Execution.

In this post we analyse the patch and discuss how effective it is.

The root cause

Let’s step back a little bit and clarify how JavaScript handles export modules. For example when we do:

export function handler() {}
export const version = "1.0"; 

at runtime, we can think of this process as a key:value mapping:

moduleExports = {
  handler: function handler() {},
  version: "1.0"
};

This object is returned when a module is loaded. So whenever we are doing

import { handler } from "./example.js";
handler();

At runtime it transpiles to:

const handlerFn = moduleExports["handler"];
handlerFn();

JavaScript is a prototype-oriented language. JavaScript objects have a prototype chain from a parent prototype. If you attempt to access a property name that does not exist in the object, JavaScript will automatically look up the object’s prototype chain.

moduleExports["handler"]; // -> explicitelly defined
moduleExports["constructor"];   //-> not an explicit export
moduleExports["__proto__"];     //-> not an explicit export

In this case:

  • handler and version are own properties.
  • constructor and __proto__ are not defined by the module, but are present on the object’s prototype (Object.prototype), and thus can be accessed through prototype lookup.

In React2Shell, this is the core issue is. The attacker can influence the value used in a dynamic lookup of the form moduleExports[INPUT]. Because this lookup is not restricted to own properties, an attacker-controlled INPUT can traverse the prototype chain and resolve unintended functions (gadgets).

In the React2Shell exploitation, we can abuse a deserialization vulnerability in React Server Components to smuggle attacker-controlled strings into React’s internal module loader. During request processing, React parses the incoming payload, revives it into JavaScript objects, and resolves special $... reference tokens that describe which server-side module and export should be loaded.

The last step happens in the function requireModule where the deserialised input is already stored inmetadata. So a payload $1:constructor:constructor becomes

metadata = [
  moduleId,       
  /* other fields */,
  "constructor"   // this becomes metadata[NAME]
];

that is then processed by the function requireModule.

export function requireModule<T>(metadata: ClientReference<T>): T {
  let moduleExports = __webpack_require__(metadata[ID]);
  if (isAsyncImport(metadata)) {
    if (typeof moduleExports.then !== 'function') {
      // This wasn't a promise after all.
    } else if (moduleExports.status === 'fulfilled') {
      // This Promise should've been instrumented by preloadModule.
      moduleExports = moduleExports.value;
    } else {
      throw moduleExports.reason;
    }
  }
  if (metadata[NAME] === '*') {
    // This is a placeholder value that represents that the caller imported this
    // as a CommonJS module as is.
    return moduleExports;
  }
  if (metadata[NAME] === '') {
    // This is a placeholder value that represents that the caller accessed the
    // default property of this if it was an ESM interop module.
    return moduleExports.__esModule ? moduleExports.default : moduleExports;
  }
   //NAME is a constant equal to 2
   return moduleExports[metadata[NAME]];
}

which performs a dynamic property lookup of the module to load:

moduleExports[metadata[NAME]]

React later invokes this resolved value as part of request handling (for example via promise resolution), which is how the returned Function object is executed.

Because metadata[NAME] contains the export name string, using names such as constructor, __proto__, or toString allows the attacker to escape the module’s export and access prototype-inherited properties.

The patch

The patch replaces the lookup with an ownership check:

- return moduleExports[metadata[NAME]];
+ if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
+   return moduleExports[metadata[NAME]];
+ }
+ return (undefined: any);

SohasOwnProperty.call(moduleExports, "constructor") will return undefined as only explicit exports are allowed.
In our previous example, the only way to pass the ownership check, would be for the input to be hasOwnProperty.call(moduleExports, "handler"). The poisoned selector in this case, can’t reach prototype-chain gadgets anymore.

This ensures:

  1. Prototype-chain properties are unreachable,
  2. Only explicitly exported symbols are callable, and
  3. Invalid selectors fail closed.

Is the patch robust?

No.

For addressing the deserialization chain based on untrusted input, hasOwnProperty limits the attacker ability. However, React still relies on the logic “allow any own property on moduleExports.”

That assumption can be shaky because the shape of moduleExports is not only controlled by React, but it’s heavily influenced by the module system or bundler. While these properties are not inherently dangerous, it does not verify which exports are safe to be called.

Conclusion

CVE-2025-55182 demonstrates, once more, the danger of unsafe deserialization and input validation. When untrusted input is allowed to influence object traversal or dispatch without strict validation, exploitation is only a matter of finding the right
gadget. The specific exploit here is novel, but the vulnerable is not.

Deserialization vulnerabilities have been around for many years and will continue to be. The vulnerable code resides in framework-bundled runtime logic rather than application code.

Developers and platform maintainers must not assume that framework-level abstractions inherently enforce safe behavior. Software supply chain failures are gaining more and more exposure, as highlighted also in the OWASP Top 10 A03:2025 Software Supply Chain Failures.

Enjoy the lab! :slightly_smiling_face:

Deco line
Deco line

Play AppSec WarGames

Want to skill-up in secure coding and AppSec? Try SecDim Wargames to learn how to find, hack and fix security vulnerabilities inspired by real-world incidents.

Deco line
Deco line

Got a comment?

Join our secure coding and AppSec community. A discussion board to share and discuss all aspects of secure programming, AppSec, DevSecOps, fuzzing, cloudsec, AIsec code review, and more.

Read more