May 19, 2026
The Flight Protocol Made Your DoS My Problem
On May 6, 2026, React and Next.js patched twelve vulnerabilities. One of them, CVE-2026-23870, is a single HTTP request that pins your Node process. The bug isn't the bug. The bug is that the framework boundary was a network boundary all along.
Sascha Becker
Author14 min read

The Flight Protocol Made Your DoS My Problem
On May 6, 2026, between one batched advisory and a slightly embarrassed pnpm up, the React and Next.js teams shipped patches for twelve vulnerabilities.1 Most of them are routine: a clutch of middleware bypasses, an SSRF in WebSocket upgrade handling, an XSS that needed a CSP nonce to land. Normal patch-Tuesday fare for a framework at this scale.
One of them is not routine. CVE-2026-23870 is a high-severity denial-of-service in React's Flight protocol deserialization path. A single crafted HTTP request, the kind of thing a bored attacker generates with curl, can pin a Node worker on excessive CPU until the runtime gives up. No authentication required. No exotic preconditions. Just a request shaped wrong on purpose, and a server that trusted the shape.
The bug itself will get patched and forgotten by next sprint. What's worth dwelling on is what the bug reveals about how we've been writing Server Components for two years.
What Flight Actually Is
A short primer first. React Server Components are components that run on the server only and never ship JavaScript to the browser. The server renders them, serializes the resulting tree into bytes, and streams those bytes to the client where React reconstructs the UI. That serialization step is Flight.
If you've shipped a React Server Component, you've shipped Flight. You probably haven't read the protocol.
Flight is the wire format React uses to ship server-rendered component trees, Server Function calls and their results, and the references between them, from the server to the client (or from one server to another). It is not React. It is not HTML. It is its own newline-delimited streaming protocol. Each line is a chunk:23
0:"Hello from the server"1:{"name":"Alice","age":30}2:["$","div",null,{"children":"$0"}]
Three chunks: a string, a plain object, and a React element whose children reference chunk 0 via "$0". The $ shows up twice and means two different things. The bare "$" inside the array is React's marker for "this is an element"; the "$0" is a pointer to another chunk, so a value used in ten places only has to be serialized once.
Each chunk has an ID in hex, an optional one-letter tag (I for module references, E for errors, Q for Maps, W for Sets, o for typed arrays), and a JSON payload. The format dedupes by construction and represents the kind of cyclic, reference-rich structures that plain JSON cannot.
This is a protocol. It is a protocol your server speaks to anyone who points an HTTP client at it. Server Actions sit directly on top of it. A Server Action is an async function you mark with "use server" and call straight from a client component, often as the action prop of a <form>. The function looks like a function. Underneath, the framework wires up an HTTP route whose request body is your function's arguments, encoded as Flight chunks and parsed by the same deserializer the server uses for inbound RSC payloads.
You exposed a wire format. The framework chose how strict the parser would be.
The Bug, In One Paragraph
CVE-2026-23870 affects react-server-dom-parcel, react-server-dom-webpack, and react-server-dom-turbopack on every released 19.x line up to 19.0.5, 19.1.6, and 19.2.5.4 The deserializer in those packages accepts inbound Flight payloads without adequately enforcing structural or type constraints. Translation: you can hand it a payload whose chunk graph is malformed, whose tags do not match the value shape, or whose references nest in ways the parser was not sized for, and the parser will burn CPU trying to make sense of it. Excessive CPU. Enough to make the worker stop answering anyone else.
The fix is the parser-hygiene work the original deserializer should have shipped with: structural validation, depth and size caps, type-tag enforcement. Patched in 19.0.6, 19.1.7, and 19.2.6. Cloudflare's WAF (web application firewall) added generic rules in front of the same class of malformed-payload attacks.5 The Next.js release covers App Router 13.x through 16.x.
Upgrade now if you haven't
If you're on Next.js App Router 13.x or later, or you import any
react-server-dom-* package directly, upgrade to a patched line today. The
attack is a single unauthenticated request. There is no graceful failure
mode to lean on while you wait for a maintenance window.
Am I Vulnerable?
Two checks. Take thirty seconds.
Check the installed version. Run one of these in your app root, depending on your package manager:
bashpnpm why react-server-dom-webpacknpm ls react-server-dom-webpackyarn why react-server-dom-webpack
If the resolved version is below 19.0.6, 19.1.7, or 19.2.6 (whichever 19.x line you are on), you ship the vulnerable deserializer.
Run the audit. Your package manager already knows about CVE-2026-23870, which has been in the GitHub Advisory Database since May 6:
bashpnpm auditnpm audit
The audit will name the CVE directly. If you depend on Next.js, it will flag the bundled react-server-dom-* even when you do not import it yourself.
The fix. Most apps just need to upgrade Next.js, which pulls the patched react-server-dom-* in as a transitive dependency:
next # latest on your major; 13.x, 14.x, 15.x, and 16.x all have patched linesreact-server-dom-webpack@19.2.6 # only if you depend on it directly; -parcel / -turbopack for those bundlers
If you want to see how many endpoints this patch is closing for you, grep for the directive:
bashrg '"use server"' --type ts -l
That count is your Server Action footprint. Even with zero matches, you are still affected: the same deserializer runs on every cached RSC payload and on the inbound RSC route Next.js mounts for client-side navigations. App Router apps are in scope regardless of whether you write Server Actions yourself.
Do not test it in production
The exploit is a single unauthenticated request, which is exactly why the responsible-disclosure writeups do not publish the payload shape. Confirm the patch by version, not by attack. If you want to reproduce the class of bug locally, use the patched packages' release notes as your starting point, against a throwaway worker on your own machine.
The Framework Boundary Was Always a Network Boundary
Here's the part that should outlast the patch.
React Server Components were sold to the audience as a render-time optimization. You write components on the server, the framework figures out which bits ship to the client as interactive JavaScript and which stay as plain HTML, and you stop paying the cost of bundling and re-running JavaScript for parts of the page that were never going to be interactive. The framing was: "It's like SSR, but better." That framing trained a generation of frontend engineers to treat the server / client boundary as an internal implementation detail, the same way they treat the boundary between a parent component and a child.
The runtime never agreed. At the runtime layer, the boundary is a serialization boundary. Every Server Action your codebase exposes is a deserialization endpoint that accepts attacker-controlled bytes. Every cached RSC payload is a record that gets fed back through the same deserializer. The 'use server' directive is not an annotation. It is an inbound HTTP handler with Flight-shaped expectations.
When the parser is permissive about shape, every component author who wrote a Server Action wrote, by accident, an unauthenticated RPC endpoint with no input validation. That description should make any backend engineer flinch. Frontend engineers writing the same code did not flinch, because the framing did not ask them to.
This is not the first time React's wire layer was the bug. Last year's CVE-2025-55182 was an RCE through Flight payload deserialization, later christened "React2Shell" in writeups.6 Same protocol. Same trust assumption. Different consequence. The DoS is the gentler cousin in the same family.
Why We Missed It
The mental-model failure is easy to reconstruct.
You write a Server Action that accepts formData. It looks like a function. You type-annotate the argument because TypeScript yells if you do not. You return { ok: true }. From inside the codebase, this is a function. From outside the codebase, it is an HTTP endpoint whose request body is parsed by a streaming deserializer for a protocol most of the team has never read.
Three things compounded.
The protocol was undocumented for most of RSC's early life. The React team treated Flight as an implementation detail. Community writeups27 reverse-engineered the format from the source. If your team's understanding of Flight comes from a blog post written by someone who read the source, your understanding is probably correct, but it is not a threat model.
The error surface of a Server Action is invisible at the call site. You cannot grep for "endpoints that accept untrusted input" in a Next.js codebase, because the endpoints are inferred from 'use server' directives. A reviewer cannot count what a reviewer cannot see.
The framework's defaults treated parsing as performance, not as security. Streaming, lazy, reference-rich deserialization is a real performance win. It also means the parser optimistically follows references before it knows the payload is well formed. A parser optimised for the happy path is a parser that allocates badly on the adversarial path.
None of these is a smoking gun on its own. Together they made the bug feel inevitable in retrospect.
What To Actually Do
Patch first. The version bumps above are the cheap part. They end this CVE. They do not end the class. Five things to put in the same PR or the next one.
1. Treat Server Actions like RPC endpoints
Every file with 'use server' is part of your public attack surface. Audit them the way you would audit a /api route. Each action gets explicit argument validation (Zod, Valibot, hand-rolled, doesn't matter), an explicit size limit on the inbound payload, and an authentication check that does not assume the action only ran because your own form pointed at it. The form is a UI. The endpoint is a contract.
ts"use server";import { z } from "zod";const Input = z.object({email: z.string().email().max(254),message: z.string().max(2000),});export async function submitContact(raw: unknown) {const { email, message } = Input.parse(raw);// ...}
Input.parse throws if the request body does not match the schema, so the rest of submitContact only runs on inputs the application actually expects. That validation step is the part you were not writing six months ago, because the framework let you skip it.
2. Cap the parser
The Flight deserializer does not expose tunables you can set at the framework boundary, but your reverse proxy does. Add request-body size caps at the edge (Vercel, Cloudflare, your nginx) for any path that accepts Server Action payloads. A normal Server Action body is in the kilobytes. Sending a megabyte is already an event worth flagging.
3. Keep your WAF rules current
Cloudflare's existing rules 2694f1610c0b471393b21aef102ec699 and aaede80b4d414dc89c443cea61680354 cover CVE-2026-23870 generically.5 Other edge providers are rolling out similar rules over the next week. The patch is necessary; the WAF is the layer that catches the variant of the same bug that drops in three months.
4. Stop trusting cached RSC payloads
GHSA-wfc6-r584-vfw7, sitting next to CVE-2026-23870 in the same advisory window, is a cache-poisoning bug in RSC. The cache for serialized component output crosses the same trust boundary as the inbound request, and the same parser fragility applies on the way back out. Anywhere your app serves RSC payloads from a cache that an attacker can influence, the cache is in scope for this class of bug.
5. Read the protocol once
Twenty minutes with the community Flight writeups23 will change how you read the next CVE. The protocol is not complicated; the issue is that we treated it as out of scope. It is not.
Defence in depth, in order
Patch the package. Validate the argument. Cap the body at the edge. Lean on the WAF for the bug you have not heard about yet. Any one of these stops the obvious attack. All four together stop the variants.
The Pattern Behind the Pattern
Every generation of "framework magic" buries a new implicit protocol inside what looks like an ordinary API.
Server Components turned components into a wire format. tRPC turned function calls into RPCs (and was at least honest about it). GraphQL turned the schema into a network surface and then spent a decade explaining query-depth limits. WebSockets turned event handlers into a long-lived TCP session with backpressure and framing. Each of these is a real ergonomics win. Each of these moved a protocol boundary into a place developers do not habitually look.
The lesson is not "don't use the magic." The lesson is that when a framework dissolves a network boundary into developer ergonomics, the threat model still owes someone a postmortem. Usually the framework team writes it. Sometimes a CVE writes it instead.
Patch the parser. Cap the body. Read the protocol. The next Flight CVE is already being written somewhere, and the version after that, and the one after that. The version of you that wrote unvalidated Server Actions had a perfectly good reason: the framework told them they did not have to. The version of you reading this now knows better.
- Vercel: Next.js May 2026 security release
Vercel's changelog entry for the May 6 advisory. Lists the affected Next.js lines and the patched versions in the form you actually need.
- Cloudflare: WAF and framework adapter mitigations for React and Next.js vulnerabilities
Cloudflare's writeup of which WAF rules cover the new CVEs and why developers cannot rely on those rules as a substitute for patching.
- Multiple Critical Vulnerabilities Patched in Next.js and React Server Components
Roll-up of the May 6 advisories. Useful for getting a sense of how broad the patch surface is across middleware, image optimization, and the Flight deserializer.
- RSC Flight Protocol (c0nrad)
Community deep dive on the Flight wire format, by someone who read the source. Worth the twenty minutes.
- Flight Protocol Syntax (React on Rails)
Reference-style writeup of chunk format, tag table, and reference syntax. Closest thing to a spec until React publishes one.
- React2Shell: lessons learned (LogRocket)
Postmortem of CVE-2025-55182, the RCE precursor in the same Flight deserialization path. Same trust boundary, sharper consequence.
- Adversis: An RSC Parser Because React Decided Wire Protocols Were Fun
A standalone Flight parser written for security research. The framing alone earns a read.
