React CVE-2026-23869 — Flight 프로토콜 DoS 취약점 분석
React Server Components의 Flight 프로토콜 역직렬화에서 발견된 DoS 취약점 — `new Map()`의 이터레이터가 트리거하는 무한 재귀를 원인부터 패치 코드까지 추적.
CVSS 7.5 — 영향 범위: Next.js 13.x ~ 16.x (App Router)
지난 글에서 RCE급 취약점(CVE-2025-55182)을 다뤘다면, 이번엔 DoS 계열의 사이클 처리 결함 한 건입니다. 핵심은 단순합니다 — "소비 플래그를 언제 설정하느냐". 이 한 줄 순서 차이가 서버 전체를 멈출 수 있었습니다.
개요
특수하게 조작된 HTTP 요청이 React Server Components에서 역직렬화될 때 과도한 CPU 사용을 유발해 서버를 다운시킬 수 있습니다. App Router를 쓰는 Next.js 13.x ~ 16.x 전반에 영향을 미칩니다.
공격 흐름

취약점의 핵심 — 순환 참조 처리 결함
React의 Flight 프로토콜은 Server Components에서 클라이언트로 데이터를 직렬화/역직렬화할 때 사용됩니다. 문제는 ReactFlightReplyServer.js의 한 함수 안에 있었습니다.
문제가 된 코드 (수정 전)
// 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;
}new Map(model)을 실행하는 순간 model의 이터레이터가 호출되고, 그 안에서 다시 createMap이 재귀 호출될 수 있습니다. 이 시점에는 아직 $$consumed = true가 설정되지 않았으므로 가드 조건을 통과하여 무한 재귀에 빠집니다.
같은 패턴이 createSet()과 extractIterator()에도 동일하게 존재했습니다.
수정 — "소비 플래그를 먼저 설정"
핵심 수정은 $$consumed = true 플래그 설정 순서를 바꾸는 것입니다. 객체 초기화 코드보다 플래그를 먼저 켜서, 재진입이 일어나는 즉시 가드에 걸리도록 했습니다.
패치 PR: facebook/react#36236
수정 후 코드
// 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;
}createMap, createSet, extractIterator 세 함수 모두 같은 패턴으로 수정됐습니다.
왜 플래그 순서만 바꾸면 막히는가
new Map()이 이터레이터를 동기적으로 실행한다
이 취약점을 이해하려면 JavaScript의 Map 생성자가 어떻게 동작하는지부터 알아야 합니다. new Map()에 배열이나 이터러블을 넘기면, 생성자는 내부적으로 Symbol.iterator를 호출해 값을 하나씩 꺼냅니다. 이 호출이 동기적으로 실행된다는 점이 핵심입니다. 즉 new Map(model) 한 줄이 실행되는 동안, model의 이터레이터 로직 전체가 완료될 때까지 현재 함수는 반환되지 않습니다.
공격자는 이 특성을 이용해 다음과 같은 악성 페이로드를 구성합니다.
// 악성 페이로드 — 자기 자신을 참조하는 이터러블
const malicious = {
[Symbol.iterator]() {
return {
next() {
// 이터레이션할 때마다 createMap을 다시 트리거
return { value: malicious, done: false }; // 절대 끝나지 않음
},
};
},
};수정 전 코드의 무한 재귀 흐름
// 수정 전 (취약한 코드)
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;
}실행 흐름을 단계별로 정리하면 이렇습니다.
createMap(model)첫 호출 →$$consumed가 false이므로 가드 통과new Map(model)실행 시작 →model의 이터레이터 즉시 동기 실행- 이터레이터 내부에서
createMap(model)재귀 호출 $$consumed가 아직 false이므로 가드 또 통과 → 2번으로 돌아감- 1~4 반복 → 콜 스택이 꽉 찰 때까지 CPU 100% 점유
플래그 설정을 new Map() 호출 이전으로 옮기면, 2단계에서 재진입하는 순간 가드에 걸려 즉시 throw됩니다. 한 줄 위치만 바꿨을 뿐이지만 사이클이 시작되기 전에 끊어지는 셈입니다.
수정 전후 비교

영향 범위 및 대응
| 영향 | 패치 버전 |
|---|---|
| Next.js 15.0.0 ~ 15.x | 15.5.15 이상 |
| Next.js 16.0.0 ~ 16.x | 16.2.3 이상 |
App Router를 쓰는 모든 Server Function 엔드포인트가 공격 대상이 될 수 있습니다. Vercel은 자사 플랫폼에 WAF 규칙을 배포해 추가 비용 없이 자동 방어 중이지만, WAF만으로 완전한 보호를 기대해서는 안 됩니다. 패치된 버전으로 즉시 올리는 게 정답입니다.
즉시 조치
- Next.js 15.x →
15.5.15이상으로 업그레이드 - Next.js 16.x →
16.2.3이상으로 업그레이드 - Vercel 호스팅이 아닌 경우 → WAF/리버스 프록시에서 비정상적으로 중첩된
Map/Set구조를 가진 Flight 요청 차단 검토
마치며
이번 취약점의 본질은 "소비 완료 표시(flag)를 언제 설정하느냐" 라는 단순한 실행 순서 문제였습니다. new Map(model)이 내부에서 이터레이터를 동기 호출하는 동안, 그 이터레이터 안에서 같은 함수가 다시 들어오는 사이클을 공격자가 악의적으로 설계할 수 있었습니다.
이런 한 줄짜리 Race Condition이 서버 전체를 멈출 수 있었다는 사실이, 직렬화/역직렬화 경로에서 사이클 보호 가드를 어디에 두느냐가 얼마나 중요한지를 잘 보여줍니다. 다음에 비슷한 코드를 짤 일이 있다면, "리소스를 만지기 전에 먼저 점유 표시를 하라"는 원칙을 떠올려 봅시다.