Sending Logs, Alerts, and Telemetry Through a Data Diode

Find Out How
We utilize artificial intelligence for site translations, and while we strive for accuracy, they may not always be 100% precise. Your understanding is appreciated.

React2Shell (CVE-2025-55182): Critical Remote Code Execution in React Server Components

By Loc Nguyen, Penetration Test Team Lead
Share this Post

CVE-2025-55182 is a critical, pre-authentication remote code execution vulnerability in React Server Components, carrying a CVSS score of 10.0 - the highest possible severity rating. As part of the OPSWAT Cybersecurity Fellowship Program, our fellows conducted a comprehensive technical analysis of this vulnerability, examining its root cause in the React Flight deserialization protocol, the full exploitation chain, and its widespread impact across the modern web ecosystem. This blog presents our findings alongside actionable guidance for defenders.

React has become one of the most widely adopted front-end libraries in the world, powering a significant share of modern web and mobile applications. Stack Overflow developer surveys consistently rank React among the top web frameworks, with adoption exceeding 40% of professional developers globally. In parallel with this growth, the React team introduced React Server Components (RSC) as a core feature of React 19 - a paradigm shift that moves rendering logic from the client to the server, enabling optimized performance and tighter integration between server-side and client-side code.

However, this architectural evolution introduced a critical new attack surface. On November 29, 2025, security researcher Lachlan Davidson reported a vulnerability in React’s server-side deserialization logic to Meta’s Bug Bounty program. Disclosed publicly on December 3, 2025, as CVE-2025-55182, the flaw enables unauthenticated remote code execution through a single crafted HTTP request. Classified as CWE-502 (Deserialization of Untrusted Data), the vulnerability requires no authentication, no user interaction, and no special application configuration - a default create-next-app deployment built for production is immediately exploitable.

Figure 1: CVE-2025-55182 (source: NVD)

The impact was immediate and severe. Within 48 hours of public disclosure, multiple exploitation campaigns were observed in the wild. According to The Shadowserver Foundation, over 77,000 public IP addresses were identified as potentially vulnerable. Cloudflare telemetry recorded more than 582 million exploitation attempts in the week following disclosure, with attack intensity averaging over 3,500 unique source IPs per hour and peaking at 16,585 simultaneous attacking IPs. Wiz Research reported that 39% of cloud environments contained vulnerable instances. CISA added the vulnerability to its Known Exploited Vulnerabilities (KEV) catalog on December 5, 2025.

Threat actors moved with notable speed and diversity. Trend Micro documented multiple campaigns - including the “emerald” and “nuts” botnet campaigns - deploying Cobalt Strike beacons, Sliver implants, the Nezha monitoring agent, Fast Reverse Proxy (FRP) tunnels, and a novel “Secret-Hunter” payload weaponizing open-source credential harvesting tools such as TruffleHog and Gitleaks. Google Threat Intelligence Group identified distinct China-nexus threat clusters (UNC6600, UNC6586, UNC6588, UNC6603) deploying specialized tooling - including the MINOCAT tunneler, SNOWLIGHT downloader, COMPOOD backdoor, and HISONIC backdoor - alongside Iran-nexus actors and financially motivated groups conducting cryptocurrency mining campaigns. AWS documented China-nexus groups experimenting with exploit code as early as December 4, before full proof-of-concept code was publicly available.

About React Server Components

React is a JavaScript library for building user interfaces, maintained by Meta and a broad open-source community. React Server Components (RSC), introduced with React 19, represent a fundamental shift in how React applications handle rendering. Unlike traditional client components that execute entirely in the browser, server components execute on the server, producing a serialized representation of the UI that is streamed to the client. This design reduces the amount of JavaScript shipped to the browser, improves time-to-interactive metrics, and enables direct access to server-side resources such as databases and file systems.

Figure 2: React Server Components (RSC)

RSC relies on a custom serialization protocol called “Flight” to encode and transmit data between client and server. When a client invokes a Server Function (formerly known as a Server Action), the browser packages the function arguments into a structured HTTP request using the Flight format. The server deserializes this payload, executes the requested function, and streams the result back to the client. This tight coupling between client and server (while architecturally elegant) means that any flaw in the deserialization logic can have immediate and catastrophic consequences, as CVE-2025-55182 demonstrates.

The vulnerability affects not only React itself but the entire ecosystem of frameworks built on top of it. Next.js (which received a separate advisory, CVE-2025-66478, later rejected as a duplicate), React Router, Waku, Parcel’s RSC plugin, Vite’s RSC plugin, and RedwoodSDK are all impacted. Even applications that do not explicitly define Server Functions may be vulnerable if RSC support is enabled in the framework.

Technical Background

Before examining the vulnerability, three foundational concepts underpin the exploit chain: JavaScript’s await behavior with thenable objects, prototype chain traversal, and the React Flight protocol’s chunk-based data model.

JavaScript’s await and Thenable Objects

The await operator pauses the execution of an async function until the awaited expression resolves. When await encounters a native Promise, it waits for settlement and returns the fulfilled value. However, await does not require a native Promise - any object with a .then() method, known as a “thenable”, is treated as a promise-like construct.

When await encounters a thenable, it invokes the object’s .then() method, passing system-provided resolve and reject callbacks. The value passed to resolve becomes the result of the await expression. Critically, if the resolved value is itself a thenable, the .then() method of that nested object is called recursively until a primitive value or a settled Promise is reached. This recursive resolution behavior is central to the exploitation of CVE-2025-55182.

Prototype Chain Traversal

Every JavaScript object maintains an internal link to its prototype, accessible via the __proto__ property. When a property is accessed on an object, the JavaScript engine first checks the object’s own properties. If the property is not found, the engine traverses the prototype chain - walking up through each __proto__ link - until the property is found or the chain terminates at undefined.

An attacker can exploit this inheritance mechanism to access properties beyond the intended scope of an object. By including __proto__ in property access paths, an attacker can reach internal methods and constructors that the application never intended to expose. In JavaScript, the expression obj.__proto__.constructor.constructor yields the global Function constructor, which can create and execute arbitrary functions from a string input.

React Flight Protocol and Chunk-Based Data Model

When a client invokes a Server Function, the browser sends an HTTP POST request with a multipart/form-data body. Each form field contains a numbered “chunk” of serialized data. The Flight protocol uses special string prefixes to encode data types: $<id> references the resolved value of another chunk, $@<id> references the raw chunk object itself, $W<id> represents a Set, $K<id> represents FormData, and $B<id> triggers the blob handler.

Consider a Server Function defined as follows:

Figure 3: Server function example

The corresponding HTTP request contains multiple form fields, each consisting of a key and a value: field 0 contains the argument array with references such as "$W1" and "$K2", while fields 1 and 2_* contain the data those references resolve to. The server processes each field as it arrives, storing intermediate results in objects called “chunks”.

Figure 4: Corresponding multipart/form-data HTTP request generated when invoking the example Server function

A chunk is a thenable object with four key properties: status (the resolution state), value (the stored data), reason (error information), and _response (a back-reference to the parent response object). When the server encounters await chunk, the chunk’s .then() method is invoked. If the chunk’s status is INITIALIZED, the resolve callback receives chunk.value. If the status is PENDING, BLOCKED, or CYCLIC, the callbacks are queued for later execution.

Figure 5: Chunk object state during deserialization

Chunk 0 typically represents the argument array for the invoked Server Function. After all form fields are received and all internal references are resolved, chunk_0.value contains the fully assembled argument array, which is then passed to the target function.

End-to-End Request Handling (Next.js → React Flight Deserialization)

The following traces how Next.js processes an incoming Server Function request under normal conditions, from the HTTP layer through the React Flight deserialization engine.

Figure 6: Next.js Server Function request handling overview

Function handleAction() - Next.js

When a Server Function is invoked, the request enters the handleAction function. This function validates metadata, checks headers and CSRF tokens, and confirms that the request is a valid fetch action. A stream called busboyStream is then created to parse the multipart form body. The decodeReplyFromBusboy function binds event emitters to this stream, triggering the React Server’s deserialization handler functions as raw data is received. The return value of decodeReplyFromBusboy is chunk_0; the await operator resolves it and passes its assembled value to the invoked Server Function.

Figure 7: handleAction function

The getChunk function returns the chunk corresponding to a given ID. If that chunk does not yet exist, it creates either a ResolvedModelChunk (if data is already present in response._formData) or a PendingChunk (if no data has arrived for that ID yet).

Figure 8: getChunk function

When decodeReplyFromBusboy returns chunk_0, the chunk is still in the PENDING state. The await operator calls chunk_0.then() and temporarily stores the resolve and reject callbacks in chunk_0.value and chunk_0.reason. These callbacks are reactivated by the wakeChunk function once reference resolution completes.

Figure 9: wakeChunk function

Deserialization Process - React Server

When busboyStream receives a complete raw data field, it triggers the 'field' event emitter, invokes the resolveField function, and initiates deserialization - converting raw form data into fully constructed JavaScript objects. The following functions govern this process.

resolveField(response, key, value)

Figure 10: resolveField function

The key and value are appended to response._formData. The function then retrieves the chunk corresponding to the ID matching the key. If that chunk already exists, resolveModelChunk is called to reconstruct it. This deferred resolution is necessary because a value may contain references to fields whose raw data has not yet arrived; in that case, the React Server creates a PendingChunk with custom resolve and reject callbacks to handle those references later.

resolveModelChunk(chunk, value, id)

Figure 11. resolveModelchunk function

resolveModelChunk creates a ResolvedModelChunk with the RESOLVED_MODEL state and injects the raw data. It then reconstructs the chunk via initializeModelChunk and calls wakeChunk to trigger any queued resolve and reject callbacks, completing the resolution of the object or reference.

initializeModelChunk(chunk)

Figure 12: initializeModelChunk function

initializeModelChunk transitions the chunk’s state to CYCLIC - indicating that reference resolution is in progress - and begins deserialization. It constructs a raw JavaScript object from chunk.value using JSON.parse, then passes this object into the reviveModel function.

reviveModel(response, parentObj, parentKey, value, reference)

Figure 13: reviveModel function

reviveModel recursively processes each component within the parsed object. When it encounters a string value, it calls parseModelString to handle it.

parseModelString(response, obj, key, value, reference)

Figure 14. parseModelString function

parseModelString dispatches on the string prefix to handle different encoded types. For references beginning with $, the getOutlinedModel function is called to resolve the cross-chunk reference.

getOutlinedModel(response, reference, parentObject, key, map)

Figure 15: getOutlinedModel function

getOutlinedModel splits the reference on the “:” delimiter to form a property access path, then traverses that path on the target chunk object to return the referenced value. As detailed in the Vulnerability Analysis below, the absence of validation on these property names is the precise point where the vulnerability exists.

Vulnerability Analysis

Root Cause

CVE-2025-55182 originates from insufficient input validation in the getOutlinedModel() function within React’s server-side Flight reply handler (ReactFlightReplyServer.js). When a chunk reference includes a property path - such as $<id>:<prop1>:<prop2> - the function resolves it by traversing the specified properties on the target chunk object, computing the result as chunk[prop1][prop2].

Figure 16: Insufficient input validation in the getOutlinedModel()

The critical flaw is that these property names are never validated. The server does not verify whether the requested properties are own properties of the object or inherited prototype properties. An attacker can therefore include __proto__ in the property path to traverse the prototype chain and reach internal methods that should never be accessible from user-controlled input. For example, the reference $1:__proto__:then resolves to Chunk.prototype.then - a function the attacker can then invoke with controlled arguments.

Figure 17: Prototype Traversal via malicious input

Vulnerable Code

The exploitation chain leverages two distinct code paths in React’s Flight deserialization logic.

The first is Chunk.prototype.then, which governs how chunks behave as thenables. When await is applied to a chunk in the INITIALIZED state, resolve(chunk.value) is called. If chunk.value is itself a thenable (an object with a .then() method) the await operator recursively invokes chunk.value.then(). This recursive resolution is the mechanism through which an attacker redirects execution to an arbitrary function.

The second is the $B (blob) prefix handler within parseModelString():

Figure 18: $B (blob) in parseModelString function

In the $B case, the function calls response._formData.get(response._prefix + id). Both _formData.get and _prefix are properties of the _response object stored inside the chunk. By controlling these properties through prototype chain traversal, an attacker can redirect this call to invoke the global Function constructor with arbitrary code as its argument.

Exploitation

Through prototype chain traversal, an attacker reaches the global Function constructor via the path <any_object>.constructor.constructor. Because Chunk.prototype.then is a function, the path $1:constructor:constructor resolves to the global Function constructor, which accepts a string and returns a callable function containing that code.

Figure 19: Global Function constructor

To demonstrate the potential real-world impact, our fellows constructed a proof-of-concept payload consistent with the independently documented analysis from multiple security research teams. The exploit operates in two stages:

Stage 1 - Construct the fake chunk:

The object delivered in field 0 acts as a fake chunk. Its then property is set to Chunk.prototype.then via the reference path $1:__proto__:then, allowing the Flight deserialization engine to invoke prototype-level behavior on this attacker-constructed object. The _response._formData.get property is pointed at the global Function constructor via $1:constructor:constructor, and _response._prefix is set to the malicious JavaScript code. The value field contains the string {"then": "$B0"}, instructing the blob handler to invoke itself on the same chunk when resolved. The status field is set to resolved_model so that initializeModelChunk is triggered when .then() is called, causing value to be parsed and the blob handler to fire.

Because field 1 has not yet been received at this point, the React Server temporarily creates resolve and reject callbacks to handle the pending reference.

Stage 2 - Trigger resolution:

Once field 1 - containing "$@0", a raw reference to chunk 0 - is delivered, the pending chunk resolves and points directly to the fake chunk. This triggers wakeChunk, which processes the queued callbacks and initiates prototype chain traversal during reference resolution. Once the fake chunk is fully resolved, wakeChunk is called again. Because the resolve callback for the fake chunk is the implicit Node.js resolution function, it invokes the chunk’s .then() method and resolves its value - ultimately constructing and executing the injected malicious code via the Function constructor.

The complete exploit requires only a single HTTP request:

Figure 20. Malicious request

Replacing {{COMMAND}} with any JavaScript code executes it on the server. The reason: -1 field prevents a toString() error during processing. The Next-Action header may contain any arbitrary value - even x - because the vulnerable deserialization occurs before the server validates the requested Server Function. This is what makes the vulnerability pre-authentication: the payload is processed during the deserialization phase, before any application-level authentication or authorization logic is reached.

Upon successful exploitation, an attacker gains full Node.js execution context on the server, including access to child_process for shell command execution, environment variables containing database credentials and API keys, the local filesystem, and cloud metadata endpoints that enable lateral movement.

Proof of Concept

Our fellows reproduced the vulnerability in a controlled lab environment using a standard Next.js application generated with create-next-app and built for production - with no modifications to the default configuration. The reproduction confirmed that the single-request exploit payload described above achieves reliable remote code execution.

Figure 21. Vulnerable Next.js web application
Figure 22. The attacker compromise the vulnerable Next.js server

The controlled demonstration showed that an attacker with network access to a vulnerable Next.js server can execute arbitrary Node.js code - including spawning a reverse shell via child_process.exec(), reading environment variables, and accessing the local filesystem - without supplying any credentials or triggering any application-level authentication checks. The arbitrary value accepted for the Next-Action header further confirms the pre-authentication nature of the flaw: the server processes and deserializes the payload before performing any action lookup or authorization check.

Mitigation

The React team released patches on December 3, 2025 - the same day the vulnerability was publicly disclosed. Fixed versions are available as React 19.0.1, 19.1.2, and 19.2.1. The patch adds strict property validation in getOutlinedModel() and reviveModel(), explicitly blocking resolution of inherited prototype properties - including __proto__, constructor, and prototype - from user-controlled reference paths in Flight payloads.

Organizations should take the following immediate actions:

  1. Upgrade React packages to a patched version (19.0.1, 19.1.2, or 19.2.1) by running npm install react-server-dom-webpack@latest, react-server-dom-parcel@latest, or react-server-dom-turbopack@latest as appropriate.
  2. Upgrade framework dependencies - Next.js, React Router, Waku, and other affected frameworks have released corresponding patches. Consult the React team advisory for version-specific upgrade paths.
  3. Do not rely solely on hosting provider mitigations - while providers such as Vercel deployed temporary WAF rules following disclosure, these are stop-gap measures and do not substitute for patching the underlying packages.
  4. Audit server logs for POST requests bearing Next-Action headers with multipart/form-data bodies containing $@ or __proto__ patterns, and monitor for unexpected child_process or execSync invocations in application logs.

Mitigating with OPSWAT

OPSWAT SBOM, a proprietary technology within the MetaDefender™ platform, provides the visibility and control necessary to defend against vulnerabilities like CVE-2025-55182. Because this flaw resides in open-source npm packages (react-server-dom-webpack, react-server-dom-parcel, react-server-dom-turbopack), organizations must first establish a complete inventory of where these components are deployed across their infrastructure before effective remediation is possible.

Figure 23. SBOM detects CVE-2025-55182

OPSWAT SBOM generates a comprehensive inventory of all software components, libraries, containers, and dependencies in use. When scanning applications or container images that include vulnerable React packages, the system automatically flags CVE-2025-55182 as Critical and provides guidance on available fixed versions, enabling security teams to prioritize and remediate before exploitation occurs.

OPSWAT SBOM is available both in MetaDefender Core™ - for scanning individual applications and container images - and MetaDefender Software Supply Chain™ - for pipeline-level visibility across your entire development lifecycle. Together, they enable security teams to:

  • Rapidly locate vulnerable components - Immediately identify which applications and containers include the affected react-server-dom-* packages at vulnerable versions, ensuring no deployment is overlooked.
  • Ensure proactive patching - Continuously monitor open-source dependencies to detect outdated or insecure packages as new advisories are published, reducing the window of exposure.
  • Maintain compliance and supply chain transparency - Meet regulatory requirements by maintaining an auditable record of all software components and their known vulnerabilities.

The speed and scale of CVE-2025-55182 exploitation - over 582 million attempts in the first week alone - underscores why reactive patching is no longer sufficient. Knowing what you have, where it runs, and when it becomes vulnerable is the foundation of a proactive defense. That visibility starts with SBOM.

References

Stay Up-to-Date With OPSWAT!

Sign up today to receive the latest company updates, stories, event info, and more.