~/hovelopin
← back to index
8 min read@hovelopin

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 전반에 영향을 미칩니다.

공격 흐름

CVE-2026-23869 공격 흐름 다이어그램

취약점의 핵심 — 순환 참조 처리 결함

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;
}

실행 흐름을 단계별로 정리하면 이렇습니다.

  1. createMap(model) 첫 호출 → $$consumed가 false이므로 가드 통과
  2. new Map(model) 실행 시작 → model의 이터레이터 즉시 동기 실행
  3. 이터레이터 내부에서 createMap(model) 재귀 호출
  4. $$consumed가 아직 false이므로 가드 또 통과 → 2번으로 돌아감
  5. 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만으로 완전한 보호를 기대해서는 안 됩니다. 패치된 버전으로 즉시 올리는 게 정답입니다.

즉시 조치

  1. Next.js 15.x → 15.5.15 이상으로 업그레이드
  2. Next.js 16.x → 16.2.3 이상으로 업그레이드
  3. Vercel 호스팅이 아닌 경우 → WAF/리버스 프록시에서 비정상적으로 중첩된 Map/Set 구조를 가진 Flight 요청 차단 검토

마치며

이번 취약점의 본질은 "소비 완료 표시(flag)를 언제 설정하느냐" 라는 단순한 실행 순서 문제였습니다. new Map(model)이 내부에서 이터레이터를 동기 호출하는 동안, 그 이터레이터 안에서 같은 함수가 다시 들어오는 사이클을 공격자가 악의적으로 설계할 수 있었습니다.

이런 한 줄짜리 Race Condition이 서버 전체를 멈출 수 있었다는 사실이, 직렬화/역직렬화 경로에서 사이클 보호 가드를 어디에 두느냐가 얼마나 중요한지를 잘 보여줍니다. 다음에 비슷한 코드를 짤 일이 있다면, "리소스를 만지기 전에 먼저 점유 표시를 하라"는 원칙을 떠올려 봅시다.

# comments