Lachlan Davidson (Carapace) tells the discovery story of React2Shell / CVE-2025-55182, the React Server Components deserialization RCE that was, per public telemetry from Cloudflare, Vercel, Fastly and Imperva, attacked over a billion times in its first week and picked up by APTs, cryptominers, and ransomware almost immediately. The setup: React's "Server Functions" send data between browser and server in a rich, undocumented, JSON-like format called Flight (the React Server Components wire protocol). Flight can encode complex JavaScript types, Maps, Sets, Dates, Promises, and can reference other parts of the payload. Davidson set out to abuse apps built on it and instead found a bug in React itself. The exploit fuses two JavaScript facts: Flight lets a payload point at React's own internal functions, and JavaScript's await keyword recursively resolves "thenables" (any object with a then method), calling attacker-influenced code. Combined, an attacker crafts a fake "chunk" whose then is invoked during deserialization and steers React's own machinery into constructing and calling a function, remote code execution. The fix hardened the deserializer; the disclosure was an unusually well-coordinated industry effort that blunted the damage.
Post 10 of my Black Hat Asia 2026 series. React2Shell is a public, patched CVE with vendor write-ups, but I'm keeping this analysis-first: the mechanism and the defenses, not a working payload. The talk and the vendor briefs carry exploit detail.
1. Why JavaScript was thought "immune" to deserialization RCE
Insecure deserialization is a classic RCE class in Java, Python (pickle), PHP, Ruby, the languages with a "history of deserialization exploits." JavaScript largely escaped it, and Davidson's explanation is simple: JSON was always "good enough." JSON encodes only objects, arrays, strings, numbers, booleans, and null, no functions, no class instances, no code, so JSON.parse can't be tricked into constructing a dangerous object. The attack surface didn't exist because the format was deliberately anemic.
That held until modern frameworks needed more. Next-generation server-side rendering, React Server Components, "Server Functions" that magically mix front-end and back-end, needed to send rich JavaScript values across the wire: streaming out-of-order chunks, complex types, and eventually data flowing client → server. JSON couldn't do that, so React built Flight. A richer format is a bigger attack surface.
2. Flight, the undocumented wire format
Flight is React Server Components' serialization protocol. Davidson had to reverse it from source, there were "❌ Docs, ❌ Specification, 🫤 read the code." Conceptually it is JSON with superpowers:
| Feature | How it's encoded (conceptually) |
|---|---|
| Base format | JSON objects/arrays |
| Complex JS types | Tagged string markers, e.g. a prefix meaning "this is a Set", "this is a Date", "this is a Blob" |
| Split into "chunks" | Numbered pieces (0={...}, 1={...}) so a big value streams in parts |
| References between chunks | A marker like $1 means "the value of chunk 1"; nested-property references exist too |
| Promises of future chunks | An async marker meaning "this value will arrive later as a Promise" |
Two properties matter for the exploit. First, chunks can reference each other and be resolved lazily, so parsing one value can pull in and evaluate another. Second, and this is the "glaring omission of a safety check" Davidson noticed early, the format lets a payload reference JavaScript built-in functions (make the server construct objects wired to Function, Array.prototype.join, etc.). On its own he "didn't think too much of it"; combined with the next piece it becomes lethal.
3. The deserialization-gadget toolkit
Before the specific bug, the talk lays out the general RCE recipe for JavaScript deserialization, useful background because "these bugs are here to stay":
- Type coercion is a hidden call-site. When JavaScript coerces an object to a string or number (
'x' + obj, template literals, comparisons), it calls the object'svalueOf/toString(ortoJSONwhen serialized/logged). If an attacker controls the object, they control which function runs. Even error paths help: Node's argument-type errors readvalue.constructor.name, another attacker-reachable call-site. - Gadgets are built-in methods repurposed. Assigning a built-in like
Array.prototype.poportoLocaleStringonto an object's method slot turns one implicit call into useful behavior, e.g. returning chosen values, or fanning one call out into many. - RCE vectors: reach
eval, or reach theFunctionconstructor (Function("...")()) and invoke the result, or reach Node built-ins (child_process,fs,module). Prototype pollution is a related path.
So the attacker's job is: plant gadget functions in a constructed object, then trigger a chain of implicit calls that ends at Function(...)() or a Node built-in. Flight gives you the "construct an arbitrary object with references to built-ins" primitive. The missing spark is a reliable trigger.
4. The spark: await recursively resolves "thenables"
This is the elegant heart of React2Shell, and it's a language feature, not a React bug. In JavaScript, await (and Promise resolution) works on any "thenable", any object with a then method. You don't need a real Promise:
let myThenable = { then: resolve => resolve("Hello!") };
await myThenable; // => "Hello!"
And crucially, resolution is recursive: if a thenable resolves to another thenable, await keeps calling then until it reaches a non-thenable value. (That's why await await await foo() is redundant, promises always "unroll.") So an attacker who can plant a then method gets their function called during any await, and can chain further calls through recursive resolution.
await decodeReply(...). That single await is the universal trigger: if the deserialized value looks like a thenable, await will call its then. The attacker doesn't need the application to do anything unusual, the framework's own await on the request body is enough. Presence + trigger now both come from React.5. Putting it together (conceptually)
Davidson found he could make the Flight payload construct a fake "chunk" object whose then points at React's own internal Chunk.prototype.then, so React's deserialization machinery treats the attacker's object as one of its internal chunks and drives it. By spoofing the chunk's internal fields and combining it with Flight's ability to reference built-ins (including the Function constructor) and its Blob-handling code path (which calls a get on attacker-influenced data), the recursive await resolution walks the object graph into constructing a function from an attacker-supplied string and invoking it, remote code execution, reachable by any endpoint that deserializes a Server Function's arguments.
Fig. 1 — Presence (a rich format that can reference built-ins) plus trigger (await's recursive thenable resolution) equals deserialization RCE. Neither half is dangerous alone.
await was meant to unwrap promises. Their intersection let values act as code.6. The blast radius, and why it wasn't worse
Public telemetry from four vendors indicates >1 billion exploitation attempts in week one (Cloudflare ~582M hits, Imperva ~127M, Vercel peaks of 2.3M/hr). It was rapidly weaponized by China-nexus APTs, and used to deploy cryptominers, ransomware (e.g. Weaxor), and novel backdoors/C2 (e.g. a Linux implant reported by Huntress). Davidson lists the reasons it wasn't even worse, which double as lessons in coordinated response:
- RSC is relatively new, so the vulnerable surface, while huge, wasn't yet universal.
- Unprecedented pre-disclosure collaboration (Meta & Vercel with major cloud vendors): WAF rules and PaaS mitigations were ready at disclosure, and Vercel ran a bug-bounty push around it.
- Threat actors were briefly distracted by faulty, AI-generated proof-of-concepts that didn't work, buying defenders time (though the bad PoCs caused their own confusion).
- It was easy to patch, a framework update rather than an app rewrite.
7. Defenses and the broader lesson
- Patch React / Next.js and the RSC packages (
react-server-dom-*) to versions with the React2Shell hardening, and keep them current, Davidson notes related CVEs (RCE, prototype pollution, DoS) in other frameworks and serializers (e.g. devalue, Seroval). This is a bug class, not a one-off. - Deploy the WAF rules the major providers shipped for CVE-2025-55182 if you can't patch instantly, they were the frontline mitigation during the billion-attempt week.
- Minimize what a Server Function trusts. Treat everything deserialized from a request as hostile; don't feed deserialized values into implicit-coercion sinks or reflect them into logs without care.
- Constrain server capability. Run Node with least privilege and, where possible, restrict access to
child_process/fsand use sandboxing so a deserialization foothold doesn't equal full host control. - For researchers: the methodology generalizes, hunt implicit call-sites (coercion, framework internals, error paths), find plantable built-in gadgets, and chain to
Function/Node built-ins. Reproducing DoS CVEs that lack public PoCs can surface latent RCEs.
Do I need a "use server" function to be vulnerable?
The vulnerable path is the framework's deserialization of Server Function arguments (decodeReply) via React's RSC packages. Any app exposing that surface on a vulnerable version is at risk. The fix is in React/RSC itself, so upgrading the framework is the primary remedy regardless of your app code.
Why did await matter so much?
Because await calls then on any object that has one, and does so recursively. The framework awaits the decoded payload, so an attacker who plants a then gets code called during deserialization without needing the application to do anything special. It's the universal trigger that turned a "construct arbitrary objects" primitive into RCE.
Is this JSON's fault?
No, the opposite. Plain JSON's poverty is exactly why JavaScript avoided deserialization RCE for years. The risk appeared when frameworks built richer formats (Flight) to carry complex types and references. Richer serialization = more gadgets.
Are other frameworks affected?
Davidson reports dozens of CVEs across other frameworks and serialization libraries (prototype pollution, DoS, and RCE). The class, rich JS (de)serialization with reachable gadgets, is general. Keep every serializer in your stack patched, not just React.
Takeaways
- JavaScript avoided deserialization RCE because JSON was too limited, until frameworks built richer formats.
- Flight (React Server Components' wire format) encodes complex types, cross-references, and, crucially, can reference JS built-ins.
awaitrecursively resolves any object with athenmethod, so the framework's ownawaiton the request is a universal trigger.- Combined: a fake "chunk" steers React's internals into constructing and calling a function, RCE (CVE-2025-55182).
- >1 billion exploit attempts in week one; rapidly used by APTs, miners, ransomware. Coordinated disclosure and ready WAF rules limited the damage.
- It's a bug class: patch all serializers, deploy WAF rules, minimize server trust and capability, and hunt implicit call-sites.
References & further reading
- Black Hat Asia 2026 — this talk's official page — abstract, speaker bio, and slides/video when posted.
- Black Hat Asia 2026 — full Briefings schedule — every talk in the conference.
- CVE-2025-55182 (React2Shell) — the React Server Components deserialization RCE.
- Cloudflare — React2Shell exploitation threat brief — ~582M hits in week one.
- Vercel — React2Shell response & bounty · Fastly · Imperva — vendor telemetry.
- Unit 42 — React2Shell & Next.js follow-on · Huntress — PeerBlight backdoor — post-exploitation.
- MDN — await & thenables — the recursive-resolution behavior at the exploit's core.