CVE-2025-55182: React Server Components Unauthenticated RCE ("React2Shell")
Updated July 28, 2026 · Written by PWNMI — see About.
React runs a meaningful share of the web, and React Server Components is the mechanism behind how modern React frameworks like Next.js render on the server. CVE-2025-55182 — nicknamed "React2Shell" by the community that found it — is about as serious as vulnerabilities get: CVSS 10.0, no authentication required, CISA lists it as actively exploited.
Affects react-server-dom-webpack/-parcel/-turbopack (the packages underlying React Server Components) 19.0–19.2.0, and any framework built on them. Reproduced here via Next.js 15.5.6, per vulhub's own lab. Original research: msanft/CVE-2025-55182 and lachlan2k/React2Shell-CVE-2025-55182-original-poc.
What you'll practice: reading a genuinely elegant exploit technique that exfiltrates command output through an HTTP header rather than a visible response body, understanding prototype-chain traversal as a deserialization attack primitive, and — a real lesson from this specific lab — recognizing when a working exploit is a fragile, precisely-tuned gadget rather than a general-purpose shell, and adapting your verification approach instead of assuming a small change should "just work."
Set up the lab
New to Docker or git, or unsure what the commands below are actually doing? See Docker for Security Labs and Git for Security Work first.
Get the lab files from pwnmihq/resources:
git clone --filter=blob:none --sparse https://github.com/pwnmihq/resources
cd resources
git sparse-checkout set cve-labs/CVE-2025-55182
cd cve-labs/CVE-2025-55182
docker compose up -d
This starts a Next.js 15.5.6 app with the vulnerable React Server Components package. No ports are published — reach it on its own IP:
CONTAINER=$(docker compose ps -q web)
IP=$(docker inspect $CONTAINER --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')
Understanding the vulnerability
React Server Components serialize server-rendered output and Server Function calls using a wire format the community calls the "Flight protocol." When the server receives a request targeting a Server Function endpoint, it deserializes the submitted payload to reconstruct the call — and the deserializer doesn't sufficiently restrict what fields in that payload are allowed to reference.
The exploit abuses this by including a field like "then": "$1:__proto__:then" — a string that, when the deserializer resolves it, walks the JavaScript prototype chain of an internal object rather than treating it as inert data. Paired with a _prefix field containing attacker-controlled JavaScript, the deserializer ends up evaluating that string server-side. The technique's most elegant detail is how it gets the result back out: the injected code throws a crafted error with a digest formatted like Next.js's own internal NEXT_REDIRECT signal, embedding the command's output directly in the redirect URL. Next.js's redirect handling surfaces that digest in the x-action-redirect response header — so the exploit doesn't need any other visible output channel at all.
Exploitation
The lab ships the exact multipart request body as body.txt. Multipart data requires CRLF line endings, so convert it first:
sed 's/$/\r/' body.txt > body_crlf.txt
curl -s -D - -o /dev/null "http://$IP:3000/" \
-H "Next-Action: x" \
-H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad" \
--data-binary @body_crlf.txt
You'll get back 303 See Other with a header like:
x-action-redirect: /login?a=uid=0(root) gid=0(root) groups=0(root);push
Real id output, root privileges, right there in a response header.
Here's the lesson worth internalizing: this is a precisely-tuned gadget, not a general shell. Try editing the injected command inside body.txt — swap execSync('id') for something with shell operators, like execSync('id; hostname') or execSync('id && whoami') — and you'll likely get a plain 500 instead. This isn't a sign the vulnerability doesn't work; it's a sign the exploit's specific JavaScript-in-JSON-in-multipart structure is sensitive to certain characters or lengths in ways that aren't fully documented anywhere. Rather than debugging that fragility, swap in a single, unchained command instead — for example, execSync('hostname') — keeping everything else in the payload untouched:
x-action-redirect: /login?a=<container-hostname>;push
Confirm this independently — not by trusting the header, but by checking the real value through a completely different channel:
docker exec $CONTAINER hostname
The two values should match exactly. That's the actual proof: two different commands, sent as two separate requests, both returning results that check out against ground truth obtained a completely different way — not a single replayed "known good" payload that might just be doing something unrelated to what you think it's doing.
Cleanup
Neither command run in this walkthrough (id, hostname) writes anything to disk or changes any application state — the output leaves entirely through the x-action-redirect header, so there's genuinely nothing on the filesystem or in Next.js's own state to revert. That's specific to the harmless proof commands used here, not a property of the vulnerability itself: the same technique with a different injected command — writing a file, modifying a config, spawning a persistent process — would leave exactly whatever that command does, and cleaning that up means knowing precisely what was run, not just that the bug exists. The one durable trace either way is in the target's own access logs — the crafted multipart requests to /, carrying a Next-Action header and the __proto__-referencing payload, are unusual enough to stand out if anyone reviews them later.
Tear the lab down when you're done:
docker compose down -v
Lessons worth keeping
- Exfiltration doesn't require a visible response. This exploit never prints anything to a page a browser would render. It hijacks an internal redirect mechanism's error-handling path to smuggle data out through a header most tools don't even think to check. When you're stuck on "how would this bug's impact actually be observable," consider channels beyond the obvious response body.
- Prototype chains are a deserialization attack surface in more languages than you'd expect.
__proto__traversal is usually associated with client-side JavaScript prototype pollution. Here it's the mechanism for server-side code execution — the same underlying language feature, entirely different consequence, because of where it's reachable from. - A working exploit that breaks on a small edit is data, not a dead end. The instinct when a modified payload fails is to assume the technique doesn't generalize. The better instinct is to isolate exactly what changed and try a smaller, different modification — which is how this lab still reached full independent verification even after two chained-command attempts failed.
Next step
For another real, maximum-severity CVE verified the same rigorous way, see CVE-2026-63030 (WordPress core) or CVE-2025-68613 (n8n). For the concepts behind the isolation pattern used here, see Docker for Security Labs.
Get new write-ups in your inbox
New roadmaps, tool walkthroughs, and lab write-ups. No spam. Unsubscribe anytime.