Skip to main content

React2Shell, and the weekend I spent patching for it

A CVSS 10 bug in React Server Components let one HTTP request run code on the server. How it works, and what I checked and patched.

Updated 6 min readMarkdown
React2Shell, and the weekend I spent patching for it

On Wednesday evening (3 December) the React team published an advisory that I had to read twice. CVE-2025-55182, which everyone is now calling React2Shell, is unauthenticated remote code execution in React Server Components, scored CVSS 10.0, exploitable in a default configuration with one crafted request.

I've spent most of the days since on it, at work and on my own projects. These are my notes.

What broke

If you use the App Router, your server and the browser talk to each other in a format called Flight. It's how React sends a rendered server component tree down to the client, and it's also how the client calls a server function. When a form submits to a "use server" action, React serialises the arguments, POSTs them to your server, and deserialises them there before your function runs.

That last step is where it went wrong. The deserialiser in react-server-dom-webpack (and the -parcel and -turbopack builds of the same code) trusted the shape of what it was given far more than it should have, and a carefully built payload could get the decoding step to reach things it was never meant to touch, and from there run code inside your Node process.

What makes it worse than the usual "please update your dependency" advisory is when it happens. It's before any of your code runs, so a server action that validates its input perfectly and checks the session is no help, because the bug is in turning the request body into arguments in the first place. And you didn't even need server actions. If the RSC server bundle is in your app, the endpoint that decodes Flight requests exists, so an app with no "use server" functions at all was still exposed.

I'm not going to describe the payload. Public proof-of-concepts appeared within about a day, and AWS reported exploitation attempts from known groups within hours of disclosure, so it's safest to assume scanners have already been at anything you have that's public.

Who is affected

Package Vulnerable First fixed
react-server-dom-webpack / -parcel / -turbopack 19.0.0, 19.1.0, 19.1.1, 19.2.0 19.0.1, 19.1.2, 19.2.1
next (App Router) 15.x, 16.0.0 to 16.0.6, some 14.3 canaries 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, 16.0.7

Next.js has its own ID, CVE-2025-66478, because it bundles a vendored copy of the React server packages. That means bumping react in package.json doesn't fix a Next app. You need the patched next.

Next.js 13.x and 14.x stable aren't affected, and neither is the Pages Router, plain client-side React (a Vite SPA, CRA) or React Native. If your React never runs on a server, you can stop reading here. Other RSC frameworks (React Router's RSC mode, Waku, the Vite and Parcel RSC plugins) shipped their own patched releases, so check their changelogs if you use one.

What I did

My side projects are small and all live on one box, so for me this was a contained job, but I did it in a deliberate order.

First an inventory, of every repo and not just the ones I remembered being on Next 15 or later:

for d in ~/Desktop/Git/profile-projects/*/web; do
  printf '%-45s ' "$d"
  (cd "$d" && npm ls next react-server-dom-webpack --depth=0 2>/dev/null | grep -Eo '(next|react-server-dom-webpack)@[0-9.]+' | tr '\n' ' ')
  echo
done

npm ls reports what's actually installed, which is what you want here. A ^15.3.0 in package.json tells you nothing about what the lockfile resolved to.

Then upgrade and rebuild from clean:

npm install next@latest react@latest react-dom@latest
rm -rf .next node_modules && npm ci && npm run build

Vercel also put out npx fix-react2shell-next, which bumps you to the right patched version for your minor line. It's fine. I did mine by hand because I wanted to see the lockfile diff.

Then redeploy everything, including the apps I'd half forgotten about, because a forgotten demo is still a Node process with your environment variables in it. After that I went looking for signs that anything had got in first: access logs for odd POSTs to page routes carrying a Next-Action header from IPs I didn't recognise, and the containers themselves for processes or files that shouldn't be there. My containers run with a read-only filesystem and no capabilities, which doesn't stop RCE but does make life a lot less comfortable for whoever lands in one.

And finally I rotated secrets on anything that had been public and unpatched once the proof-of-concepts were out, because you can't prove nobody read process.env. For me that was a couple of database passwords and a webhook key. It's the step people skip, and it took less time than writing this paragraph.

Afterwards

Server functions feel like calling a function, but they're really a public POST endpoint with a clever serialisation format, and that format is now part of your attack surface in the same way JSON and multipart parsers always have been.

The managed platforms got WAF rules out quickly, which helped people on Vercel and Cloudflare. When you self-host nobody does that for you, and your real defence is how quickly you can go from advisory to patched build. For me that's one deploy script per app, and this week I was very glad of it. I'll write separately about how this has changed the way I write server actions.

Update (12 December 2025): there's a second round. People looking at the patch found two more problems in the same code: CVE-2025-55184, a denial of service where a crafted request can hang the server in an infinite loop, and CVE-2025-55183, where a request can get a server function's source code back. Neither is RCE, but the second one leaks anything hardcoded in a server function. The first DoS fix also turned out to be incomplete (CVE-2025-67779), so the versions to be on now are React 19.0.3, 19.1.4 or 19.2.3, and Next.js 16.0.10 or the latest patch of your 15.x line. If you patched on the 4th, you need to patch again.


First published on my blog. Cover photo: Decreasing volume, CC BY-SA 4.0, via Wikimedia Commons (source).

About the author

  • Full stack engineer in London with 5+ years on production web platforms. I lead AI-assisted modernisation at Xanda: React and Go for the UK energy market, a government claims portal, and moving old Java and C++ systems onto the cloud. Outside work I run a small home server behind a Cloudflare Tunnel with a handful of public tools on it: Doorstep (rental alerts across Rightmove, OnTheMarket and OpenRent), SeenSooner (NHS A&E and planned-care waits), GridPulse (live GB grid carbon), Sift (LLM ticket triage that survives model failures) and AI Radar (a news digest written hourly by an n8n workflow). I write here about React and Next.js upgrades, Node, coding with AI agents, and the CVEs I've had to patch. Originals live at blogs.kishankanejiiya.tech.

No comments yet. Be the first to comment.