Go back

What's Next: React2Shell Beyond Next.js

avatar
Jonathan Peterson@lobsterjerusalem
avatar
Cale Blackhosakacorp.net

Key Takeaways

Next.js has been a primary target for React2Shell exploitation, but there are various other vulnerable frameworks that are exploitable when React Server components (RSC) is enabled.
VulnCheck has developed exploits for four vulnerable frameworks outside of Next.js: React RSC, React Router, Expo, and Waku.
In this blog, we investigate exploitation patterns for each of the frameworks and the impact of subtle differences on detection and exploitation.

What's Next: React2Shell Beyond Next.js

Since React2Shell (CVE-2025-55182) hit the first week of December, VulnCheck’s Initial Access Intelligence (IAI) team has been hard at work exploring initial access exploit scenarios to give customers the best possible understanding of both known and likely attack vectors. While Next.js is understandably the focus area for many response teams given its huge deployment footprint, VulnCheck has also verified and developed functional exploits for four additional React2Shell exploit variants that target frameworks beyond Next.js. Most of these other variants require the experimental React Server Components (RSC) functionality to be explicitly enabled (and used in a particular manner), so we wouldn’t expect them to present the same type of ready-to-go attack surface that default Next.js applications offered. Still, we believe it’s worth considering additional remote attack vectors, even if they don’t have the scale and broad appeal of vulnerable Next.js apps.

In this blog, we will look at React2Shell exploit variants that have been paid much less attention — and in some cases, virtually no attention at all.

Additional Frameworks

The original React disclosure blog on CVE-2025-55182 notes that a number of different frameworks are vulnerable, including but not limited to Next.js:

  • Next.js
  • Vite RSC plugin
  • Parcel RSC plugin
  • React Router RSC preview
  • RedwoodSDK
  • Waku

Next.js certainly offered the largest attack surface area, but once a public exploit targeting Next.js applications was out, the rest of the frameworks seemed to fall by the wayside in the industry discourse on this vulnerability. The VulnCheck research team spent some time assessing these less-examined frameworks to see what React2Shell exploit variants might need to look like and how we could provide our customers with specific coverage for those variants.

The team has broken down each vulnerable framework we have looked at so far, highlighting similarities and differences we found in payload development and fingerprinting patterns. This information is intended to aid others in the community for not only building out their own defense and detection capabilities, but also to offer new insight that can aid organizations in their own investigations into potentially vulnerable frameworks beyond Next.js .

React RSC

The React project itself, where CVE-2025-55182 originated, gives a good baseline for understanding how handling is shared across the other frameworks and where exploitable components propagated into the rest of the ecosystem. Unlike Next.js applications that utilize vulnerable RSC functionality by default, the core React RSC functionality requires explicitly usage of RSC in the application in order for it to be exploitable; in other words, RSC has to be a feature of the bundler, or used in use server directives.

We first looked at the Parcel bundlers RSC integration and set up a small demo application from the project's example repositories. The requests to the RSC endpoint may look familiar to those who have analyzed React2Shell internals:

There are few things that stand out in the request that do not match Next.js exploitation patterns. The primary request differences relate to the rsc-action-id header and the $ACTION_ID variable that share the generated variable name, in this case a13z6#createTodo. Modification of the body $ACTION_ID changes some of the internal paths, but doesn’t always seem to be required to hit the known code paths. The same cannot be said for modification of the rsc-action-id, which if modified on our Parcel-built app throws the following error:

From our testing with other bundlers and applications, that header is always required to correspond to a function on the server. While this might seem like an issue from an attacker's perspective, as it may be difficult to identify who is using the RSC integration, there are client-side effects of utilizing RSC functions that appear to have been largely missed in public analysis of React2Shell: the clients serve generated blobs on pages with client RSC integration that are fingerprintable.

For the Parcel app, the client gets sent a set of JavaScript blocks containing the a13z6#createTodo variables; these also contain an easy-to-fingerprint self.__FLIGHT_DATA variable, such as this truncated example we see from visiting our applications page:

An additional fingerprint is left behind if the application uses form data, and that can be seen being rendered directly in the HTML form element name:

The combination of these two gives us all we need to effectively copy and paste payloads from the other React2Shell exploits. In our exploitation experiments, we simply modified our exploits to search the HTML for the action ID forms — or, if those were not available, we extracted the Flight data variables from the functions and applied those to the rsc-action-id header:

Based on our analysis, this means that attackers do have a relatively low-touch way to enumerate functions when they correspond to the React RSC integration, which may allow attackers who can reach these functions to fingerprint potential victims and automate exploitation steps.

React Router

The React Router project also contains experimental support for RSC, which requires explicitly enabling the RSC integration via RSC Framework Mode. Based on our testing and canary development, it appears that enabling RSC Framework Mode inherits the requirement that rsc-action-id headers be set, but luckily (for attackers) it doesn’t seem like that value has to correlate with an actual set value to reach the vulnerable sink.

This means that a vulnerable React Router application will just need any action ID header value set and it will be exploitable by the same payload values as the React RSC and Next.js.

Expo

The Expo framework also includes experimental support for RSC that requires explicitly enabling the RSC integration with a reactServerFunctions flag. Once that flag is enabled and RSC is built into a client endpoint, it appears at first that the Expo project generates its HTML document in a similar way to the core React RSC integration; but as it turns out, Expo bundles the RSC client-facing JavaScript functions into an entry.bundle file that is accessible from /node_modules/expo-router/entry.bundle?platform=web. The platform variable does appear to be required, but web was consistent in our testing. That bundle resource contains all the RSC function calls.

The requests to the Expo RSC functionality quickly diverge from the vanilla RSC integration by requiring that each of the function calls goes to a specific path in place of the header value. For example, our test application would send RSC values to /_flight/web/ACTION_./functions/render-home.tsx/renderHomeAsync.txt and would set the expo-platform header to match the platform sent in the retrieved bundle. The requests to those endpoints then allow for usage of the same payloads as with the previous frameworks.

Fully automated exploitation is possible on Expo by combining the consistent path for entry.bundle, the extraction of functions to predictable path formats, and the known headers:

Waku

Waku markets itself as a “Minimal React framework”, since like these other targets Waku is of course, a React framework that supports RSC to facilitate server-side rendering. You don’t have to look far on their homepage to figure this out, either — one of the first things mentioned after “Getting started” is the rendering that Waku offers, via RSC.

Spinning up a test target for this one is simple; in their getting started guide, the project developers show a one-liner to quickly stand up a test project using npm create waku@latest. Of course if you were to use this exact version today, you would get a project running a patched version of React on the back end. While it’s great that they’ve patched it all up, we obviously need an unpatched version.

Running npm view create-waku versions should give you a list of viable candidates, and you can just pick an older one. We used npm create waku@0.12.4-0.26.0-alpha.2-0.

When you run that command, it will prompt for a project name: just hit enter, then follow the instructions to cd into the directory and start it with npm run dev, which serves the project at port 3000.

What you end up with is a simple page like this:

So now that it is up and running, we just need to see what is going on behind the scenes to get us a starting point to start trying to “port” the React2Shell exploit to this target.

We hook up the browser to BurpSuite and do a “Hard-Refresh” on the page, then click on the “About page” link since that is about the only thing you can do here.

Nothing interesting appears to happen, but looking at the BurpSuite log you can see a particularly interesting request that went across when we clicked on the “About page” link.

GET /RSC/R/about.txt?query= HTTP/1.1
Host: localhost:3000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost:3000/
X-Waku-Router-Skip: ["page:/","layout:/","root","route:/"]
DNT: 1
Sec-GPC: 1
Connection: keep-alive
Cookie: sessionId=fdce4ef9-83f1-44c7-aa10-0d61849ce6ee
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0

It’s right there in the URL, /RSC/, and that same .txt file extension that was also present in the Expo framework.

So this feels like a solid starting point. Like the other payloads, we will change the GET to POST and then keep the URL and headers that are already present in the site-generated request; here, we also slap in a multipart Content-Type header and a version of the React2Shell payload. This new request ends up looking like this:

You can see the JavaScript “payload” in there: console.log(\“hi there neighbor\”);

Hitting send on this request causes it to hang, which if recent history is any indication, is exactly what we want.

The terminal running npm run dev confirms this:

(.ia) lobsterjerusalem /tmp/yy/waku-project $ npm run dev

> waku-project@0.0.0 dev
> waku dev

ready: Listening on http://localhost:3000/
[vite] connected.
[vite] connected.
hi there neighbor

At this point we thought we would just slap a child_process payload into this and call it done.

With a new payload of: require(\"child_process\").exec(\"id>/tmp/pr00f\"); ready to go, we hit send, expect a hang, and instead get a 500 error.

Looking at the terminal output again, we see a giant bummer:

ReferenceError: require is not defined
    at Object.eval [as then] (eval at parseModelString 
… snipped for brevity …

There’s no require() call available where we landed.

Now in a previous payload variant that we worked on for Next.js, there was a process.mainModule which had a require() method. So we employ the same general idea here. You can enumerate the methods available using payloads like console.log(Object.getOwnPropertyNames(global/process/whatever variable)); and then check the output in the terminal that is serving Waku. While there may be other candidates, we eventually found process.getBuiltinModule() to be a viable require() equivalent.

As a result, we swap out the payload for console.log(process.getBuiltinModule(\"child_process\").execSync(\"echo A\")); which prompts our terminal to output <Buffer 41 0a> a.k.a (“A\x0a”) — meaning it worked!

There’s one last step, but it would serve readers well to remember we are running this using npm run dev, which is dandy for our purposes; others may run it in a production context. Usually with npm projects this ends up being npm run start but you can look at the “scripts” section of the package.json just to be sure.

Executing npm run start does result in an error, but one that is easily fixed by first running npm run build before attempting npm run start again.

The production version is now running on port 8080, so we just adjust the settings in Burp’s repeater to send it there instead (as well as the host header, for good measure).

We hit send and got a 200 status code in response…and the command failed to execute.

At this point we thought the problem might be related to the header, since that seemed to be a defining factor in the other React2Shell framework targets we tested. We began reading the code on Waku’s GitHub pertaining to header parsing.

Our problem turned out not to be related to the header. In fact, unlike the other frameworks, Waku does not require its own X-Waku-Router-Skip header at all; we removed the header, but it had no bearing on the success of the exploit.

After a bit more trial and error, we decided to do a light bit of fuzzing on the URL. As it turns out, all that is required to execute the payload in Waku’s “production” context is to ensure that the endpoint does not exist, though it still needs to have the .txt extension and be behind the /RSC/ path. So in short, /RSC/<any valid URL characters>.txt will do the trick as a viable URL for exploitation in both dev and production contexts.

Here is a final functioning payload to end this on:

If it hangs, it has likely executed the provided payload and RCE has been achieved.

Additional insight into React2Shell exploitation, PoCs, and payloads can be found in the following blogs: Critical Vulnerability in React and Next.js (CVE-2025-55182), Reacting to Shells: React2Shell Variants & the CVE-2025-55182 Exploit Ecosystem, React2Shell and What Our Canaries See, and React2Shell Exploits on GitHub.

About VulnCheck

The VulnCheck research team is always looking for new vulnerabilities to analyze and curate. For more research like this check out our blogs The Mystery OAST Host Behind a Regionally Focused Exploit Operation, XWiki Under Increased Attack, and Fortinet FortiWeb Exploitation Hits Silently Patched Vulnerability.

Sign up for the VulnCheck community today to get free access to our VulnCheck KEV, enjoy our comprehensive vulnerability data, and request a trial of our Initial Access Intelligence, IP Intelligence, and Exploit & Vulnerability Intelligence products.

Ready to get Started?

Explore VulnCheck, a next-generation Cyber Threat Intelligence platform, which provides exploit and vulnerability intelligence to help you prioritize and remediate vulnerabilities that matter.
  • Vulnerability Prioritization
    Prioritize vulnerabilities that matter based on the threat landscape and defer vulnerabilities that don't.
  • Early Warning System
    Real-time alerting of changes in the vulnerability landscape so that you can take action before the attacks start.