This morning, an advisory was released for Next.js about a vulnerability that leads to RCE in default configurations, with no prerequisites. The root cause of this issue lies in React Server Components, which Next.js utilizes.
Over the last day, we have noticed an incredible amount of incorrect PoCs floating around on GitHub that do not realistically confirm the presence of this vulnerability with high confidence.
Some PoC’s we have seen on GitHub completely misdiagnose the root cause of the vulnerability, and the key fact that it can be exploited on Next.js without any prerequisites, such as the presence of certain functions inside the context.
While we have provided the HTTP request and expected response below, if you prefer to use tooling to detect this issue across a list of hosts, please see our GitHub repo for this vulnerability: https://github.com/assetnote/react2shell-scanner.
The original author of this vulnerability confirms that these PoC’s on GitHub are not the same exploit that was shared with React and Next.js maintainers. See the author’s message on https://react2shell.com/.
While there are several mechanisms to determine whether an asset is running React Server Components (RSC), simply verifying that RSC is present is not a high-enough signal to determine whether an asset is truly vulnerable to this RCE.
As a result, our Security Research team investigated to identify an HTTP request that can be used to safely and confidently confirm the presence of this RCE vulnerability in Next.js applications.
The following HTTP request can be used to confirm the presence of the vulnerability:
POST / HTTP/1.1
Host: hostname
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36 Assetnote/1.0.0
Next-Action: x
X-Nextjs-Request-Id: b5dce965
Next-Router-State-Tree: %5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9
Content-Length: 232
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
{}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
["$1:a:a"]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
The HTTP response from a vulnerable version of Next.js when sending the request above will look like the following:
HTTP/1.1 500 Internal Server Error
Date: Thu, 04 Dec 2025 06:16:39 GMT
Content-Type: text/x-component
Connection: keep-alive
Cache-Control: no-store, must-revalidate, no-cache, max-age=0
Vary: rsc
Content-Length: 76
0:{"a":"$@1","f":"","b":"yd-J8UfWl70zwtaAy83s7"}
1:E{"digest":"2971658870"}
Checking for the presence of E{"digest" and a 500 status code, will reliably return vulnerable hosts in your environment.
The reason that this check is able to differentiate vulnerable hosts from non-vulnerable ones is due to the way the React-Server dependency uses a colon to delimit object properties. See the code snippet below:
function getOutlinedModel<T>(
response: Response,
reference: string,
parentObject: Object,
key: string,
map: (response: Response, model: any, parentObject: Object, key: string) => T,
): T {
const path = reference.split(':');
// ... snip ...
for (let i = 1; i < path.length; i++) {
value = value[path[i]];
}
For example, if we pass JSON in a multipart request like so:
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
["$1:a:b"]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
{"a":{"b":"foo"}}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
This will be transformed into the following:
"$1:a:b" -> {"a":{"b":"foo"}}.a.b -> "foo"
In the vulnerable versions of React Server, we can force a 500 error through the following multipart request:
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
["$1:a:a"]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
{}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
The reason that this causes an exception is that it ultimately maps to the following:
"$1:a:a" -> {}.a.a -> (undefined).a -> 500
Patched versions of React Server Components added an additional check to this colon notation, which prevents a crash from occurring:
const name = path[i];
if (typeof value === 'object' && hasOwnProperty.call(value, name)) {
value = value[name];
}
This means if the : syntax references a property that doesn’t exist, it is ignored. This means post-patch, we don’t get a 500 anymore.
Customers of Assetnote’s Attack Surface Management Platform received a notification of this vulnerability across their entire attack surface earlier today via this high-fidelity check developed by our Security Research Team.
Searchlight Cyber’s ASM solution, Assetnote, provides industry-leading attack surface management and adversarial exposure validation solutions, helping organizations identify and remediate security vulnerabilities before they can be exploited. Customers receive security alerts and recommended mitigations simultaneously with any disclosures made to third-party vendors. Visit our attack surface management page to learn more about our platform and the research we do.