React CVE-2026-23869: a DoS in Flight deserialization
A denial-of-service bug in React Server Components' Flight protocol — how the iterator behind new Map() triggers unbounded recursion, traced from root cause to the patch.
CVSS 7.5 — Affected: Next.js 13.x through 16.x (App Router)
The previous post (in Korean) covered an RCE-class bug, CVE-2025-55182. This one is a DoS-class cycle-handling flaw. The whole story comes down to one question: when do you set the "consumed" flag? That single ordering difference was enough to stall an entire server.
What is the problem
A specially crafted HTTP request, when deserialized by React Server Components, can drive CPU usage high enough to take the server down. Every Next.js release from 13.x to 16.x that uses the App Router is affected.
The attack path
A single request reaches a stalled server in five steps.
CVE-2026-23869 — 페이로드 하나가 CPU 를 붙잡기까지

The cycle-handling flaw
React's Flight protocol serializes and deserializes data between Server Components and the client. The bug lived inside one function in ReactFlightReplyServer.js.
The vulnerable code (before the fix)
// createMap() — 취약한 순서
function createMap(response, model) {
if (model.$$consumed === true) throw new Error("Already initialized Map.");
const map = new Map(model); // ← 여기서 순환 참조 재진입 가능
model.$$consumed = true; // ← 플래그 설정이 너무 늦음
return map;
}The moment new Map(model) runs, model's iterator is invoked, and from inside that iterator createMap can be called again. At that point the $$consumed flag has not been set yet, so the guard passes and the function falls into unbounded recursion.
The same pattern existed in createSet() and extractIterator().
Set the consumed flag first
The core fix is to set the $$consumed flag earlier. By raising the flag before the object is initialized, any re-entry hits the guard immediately.
Patch PR: facebook/react#36236
After the fix
// createMap() — 수정 후
function createMap(response, model) {
if (model.$$consumed === true) throw new Error("Already initialized Map.");
// ✅ 플래그를 먼저 설정 → 재진입 시 즉시 throw
model.$$consumed = true;
const map = new Map(model);
return map;
}All three functions — createMap, createSet, and extractIterator — were changed the same way.
Why reordering one line is enough
new Map() runs the iterator synchronously
To understand this bug you first need to know how JavaScript's Map constructor behaves. When you pass an array or any iterable to new Map(), the constructor calls Symbol.iterator internally and pulls values one at a time. The key detail is that this happens synchronously. While the single line new Map(model) executes, the current function does not return until model's entire iterator logic has finished.
An attacker exploits that property with a payload like this.
// 악성 페이로드 — 자기 자신을 참조하는 이터러블
const malicious = {
[Symbol.iterator]() {
return {
next() {
// 이터레이션할 때마다 createMap을 다시 트리거
return { value: malicious, done: false }; // 절대 끝나지 않음
},
};
},
};How the pre-fix code recurses forever
// 수정 전 (취약한 코드)
function createMap(response, model) {
if (model.$$consumed === true) {
throw new Error("Already initialized Map.");
}
// ↑ 처음 호출 시 false이므로 가드를 통과
const map = new Map(model);
// ↑ 이 순간 model[Symbol.iterator]() 즉시 실행됨
// → 이터레이터 안에서 다시 createMap(model) 호출
// → 이 시점에 $$consumed는 아직 false
// → 가드를 또 통과 → 또 new Map(model) → 무한 재귀!
model.$$consumed = true; // ← 여기까지 영원히 도달하지 못함
return map;
}Step by step, the execution looks like this.
- First call to createMap(model) →
$$consumedis false, so the guard passes - new Map(model) starts → model's iterator runs immediately and synchronously
- Inside the iterator, createMap(model) is called recursively
$$consumedis still false, so the guard passes again → back to step 2- Steps 1–4 repeat → the CPU sits at 100% until the call stack overflows
Move the flag assignment before the new Map() call, and the re-entry in step 2 hits the guard and throws right away. Only one line moved, but the cycle is cut before it can begin.
Before and after
createMap 재진입 — 플래그를 언제 세우느냐에 따라 갈리는 콜 스택

Affected versions and response
| Affected | Patched in |
|---|---|
| Next.js 15.0.0 ~ 15.x | 15.5.15 or later |
| Next.js 16.0.0 ~ 16.x | 16.2.3 or later |
Every Server Function endpoint behind the App Router is a potential target. Vercel has deployed WAF rules across its platform, defending automatically at no extra cost, but do not treat the WAF as complete protection. The right answer is to upgrade to a patched version immediately.
Immediate actions
- Next.js 15.x → upgrade to 15.5.15 or later
- Next.js 16.x → upgrade to 16.2.3 or later
- Not hosted on Vercel → consider blocking Flight requests with abnormally nested Map/Set structures at your WAF or reverse proxy
Closing thoughts
At its core this vulnerability was a plain ordering problem: when do you mark something as consumed? While new Map(model) was synchronously driving the iterator, an attacker could design that iterator to re-enter the same function and form a cycle.
That a one-line race condition could stall an entire server is a sharp reminder of how much it matters where you place cycle guards on serialization and deserialization paths. The next time you write code like this, remember the rule: mark the resource as taken before you touch it.