infiltr8 Lab
Draft · 검수 전 · 심층판
Home/ CVE/ 웹·서버/ React2Shell
CVE · 웹·서버 · RCE · CVE-2025-55182
심층 분석 연구 분석

데이터를 되살리려던 코드가, 코드를 되살렸다

React2Shell(CVE-2025-55182) — 인증 없이 요청 한 번으로 서버에서 임의 코드가 실행되는 CVSS 10.0 취약점. 왜 성립하는지를 코드가 도는 순서대로 분석합니다.

공개된 취약점을 우리 관점에서 직접 분석·재구성한 연구 글입니다. 코드 발췌는 React 오픈소스(MIT)의 실제 소스입니다.

infiltr8 team 직접 분석 #prototype-pollution#RSC#deserialization
무엇React 서버 컴포넌트의 Flight 역직렬화에서 발생하는 무인증 원격 코드 실행. Next.js App Router는 기본 설정만으로 노출됩니다.
원리데이터를 복원하는 코드가 복원 대상의 정체를 세 번 잘못 믿어, "복원"이 "코드 실행"으로 바뀝니다.
핵심참조 경로를 따라갈 때 소유 검사(hasOwnProperty)가 없어 __proto__로 프로토타입에 닿습니다. 이 한 줄이 취약점의 전부입니다.

01개요

React 서버 컴포넌트(RSC)의 Flight 역직렬화 경로에서 발생하는 무인증 원격 코드 실행입니다. Next.js App Router 조합은 기본 설정만으로 노출됩니다.

데이터를 복원하는 코드가 복원 대상의 정체를 세 번 잘못 믿어, "복원"이 "코드 실행"으로 바뀝니다.

CVSS: 10.0

관련 식별자: CVE-2025-55182 / CVE-2025-66478(Rejected).

02영향받는 버전

대상취약 버전안전 버전
React19.0.0 / 19.1.x / 19.2.019.0.1 · 19.1.2 · 19.2.1 (라인별)
Next.js15.x(~15.5.6) · 16.x(~16.0.6) · 14.3.0-canary.77+15.0.8 · 15.1.12 · 15.2.9 · 15.3.9 · 15.4.11 · 15.5.10 · 16.0.11 · 16.1.5 (캐너리: 15.6.0-canary.60 · 16.1.0-canary.19) — 2026-08-29 react.dev 기준값. 후속 CVE 대응으로 갱신될 수 있으니 설치 전 react.dev 원문 확인

03배경 — 역직렬화는 작은 인터프리터다

React 서버 컴포넌트(RSC)는 서버와 브라우저가 복잡한 데이터를 주고받습니다. 일반 JSON으로는 Promise나 Blob을 표현할 수 없어, React는 Flight라는 자체 포맷을 씁니다.

값을 chunk 조각으로 쪼개고, 조각 사이 참조를 $@·$B 같은 문자열로 적어 둡니다. 서버는 이 참조를 따라가며 원래 객체로 되살립니다 — 이게 역직렬화입니다.

여기서 하나만 미리 짚습니다. 입력을 받아 객체로 복원하는 코드는 사실상 작은 인터프리터입니다. 자바 가젯 체인, 파이썬 pickle이 반복해 뚫린 이유가 이것입니다.

복원 과정이 "무엇을 만들지"를 입력이 조종할 수 있으면, 역직렬화는 그 순간 코드 생성 도구가 됩니다.

04취약점 발생 원인

취약점은 서버가 요청을 처리하는 흐름을 따라 세 지점에서 차례로 열립니다. 아래 그림이 전체 사슬이고, 이어서 각 지점을 하나씩 봅니다.

1"status가 resolved이면, 완성된 내 조각이다"

핵심

조각의 정체를 상태 값 하나로 판단합니다 — 그런데 그 상태 값은 공격자가 붙일 수 있는 필드입니다.

Flight의 조각(Chunk)은 Promise를 상속한 thenable, 즉 then을 가진 객체입니다. 자바스크립트는 then이 있는 객체를 Promise로 취급해 await합니다 — 정체가 아니라 모양으로 판단합니다.

그래서 조각 해석 코드는 then이 불릴 때 status"resolved_model"이면 '완성된 정상 조각'으로 보고 곧장 파싱에 넘깁니다. 공격자가 {"status":"resolved_model"}를 붙여 보내면 그대로 통과합니다.

왜 이렇게 됐나이 역직렬화기는 "내가 처리하는 객체는 전부 내가 만든 것"이라는 전제 위에서 쓰였습니다. 외부에서 "나는 완성된 조각이야"라고 스스로 주장하는 객체가 들어올 상황을 고려하지 않았습니다.

2"참조 경로의 각 조각은 데이터 접근일 뿐이다"

핵심

경로의 키를 소유 검사 없이 따라가기 때문에, __proto__ 한 조각으로 데이터가 아니라 프로토타입에 닿습니다.

$1:foo:bar는 '1번 조각의 foobar'라는 참조입니다. 복원 코드는 이를 :로 쪼갠 뒤 속성을 하나씩 따라 들어갑니다. 문제는 각 조각을 키로 쓰면서 그 키가 객체 자신의 속성인지(hasOwnProperty)를 확인하지 않는다는 것입니다. 아래 615행이 바로 그 무방비한 접근입니다.

function getOutlinedModel<T>( response: Response, reference: string, parentObject: Object, key: string, map: (response: Response, model: any) => T, ): T { const path = reference.split(':'); const id = parseInt(path[0], 16); const chunk = getChunk(response, id); switch (chunk.status) { case RESOLVED_MODEL: initializeModelChunk(chunk); break; } // The status might have changed after initialization. switch (chunk.status) { case INITIALIZED: let value = chunk.value; for (let i = 1; i < path.length; i++) { value = value[path[i]]; }

React 실제 소스 — ReactFlightReplyServer.js · getOutlinedModel (패치 전, 커밋 36df5e8b — 패치 커밋 7dc903c의 부모). 붉은 615행이 소유 검사 없이 임의 키로 속성에 접근하는 곳입니다.

자바스크립트에서 obj["__proto__"]obj["constructor"]는 데이터가 아니라 객체 내부로 통하는 통로입니다. 게다가 Flight의 $@ 참조는 조각의 값이 아니라 조각 객체 자체를 돌려줍니다.

그래서 $1:__proto__는 조각들이 공유하는 Chunk.prototype이 되고, $1:constructor:constructorFunction 생성자가 됩니다. Function("문자열")은 문자열을 코드로 컴파일하는 가장 강력한 도구입니다.

검증 없는 경로 접근 하나가 '임의의 문자열을 코드로 만드는 능력'을 통째로 넘긴 셈입니다.

왜 이렇게 됐나작성자는 참조 경로를 JSON 포인터 같은 "순수한 데이터 탐색"으로 취급했습니다. 하지만 JS에서 임의 문자열 키로 속성에 접근하는 순간, 그 키가 __proto__라면 값 읽기가 아니라 정체를 만지는 일이 됩니다.

3"데이터를 꺼내는 이 함수는 내가 아는 그 함수다"

핵심

평범한 조회 함수가 이미 Function 생성자로 바뀌어 있어, 데이터를 꺼내려는 호출이 곧 코드 컴파일이 됩니다.

복원 중 Blob 참조($B)를 만나면, 코드는 원본 데이터를 꺼내려 내부 조회 함수(_formData.get)를 호출합니다. 그런데 두 번째 결함으로 이 함수는 이미 Function 생성자로 바뀌어 있습니다 — 데이터를 꺼내려는 호출이 곧 공격자 문자열을 코드로 컴파일하는 일이 됩니다.

같은 오염으로 이 객체의 then은 정상 조각의 then을 가리켜, 객체 전체가 thenable이 됩니다. React가 이를 해석하며 promise를 resolve하는 순간 방금 컴파일된 코드가 실행됩니다 — 인증도, 클릭도 없이.

분석 결론

세 단계 사슬처럼 보이지만, 무게 중심은 두 번째 결함(경로 소유 검사 부재) 하나입니다.

첫 번째(가짜 status)와 세 번째(함수 치환)는 단독으로는 무력합니다. 두 번째 결함이 Chunk.prototypeFunction 생성자로 가는 길을 열어야 비로소 무기가 됩니다. 그래서 보안상 핵심은 "이 키가 객체 자신의 속성인가"를 묻는 한 줄입니다 — 실제로 배포된 패치는 이 한 줄을 포함한 더 큰 리팩터의 일부였지만, 사슬을 끊는 지점은 여전히 이 한 줄입니다.

한 문장으로 묶으면 이렇습니다. 역직렬화기는 자기가 다루는 것의 정체를 세 번 잘못 믿었습니다 — 스스로 선언한 상태를, 경로에 섞인 프로토타입 통로를, 자기 내부 함수의 정체를. 그 잘못된 믿음이 하나씩 공격 재료가 됐습니다.

05공격 과정

5.1 공격 과정 요약

공격자 요청 · 가짜 chunk status = "resolved_model" ① 자기 선언 status를 검증 없이 신뢰 역직렬화 시작 initializeModelChunk → reviveModel ② 경로에 __proto__ / constructor · 소유 검사 없음 핵심 지점 Chunk.prototype 접근 + Function 생성자 탈취 ③ _formData.get 을 Function 생성자로 치환 문자열이 실행 가능한 함수로 컴파일 promise resolve 서버에서 임의 코드 실행 Remote Code Execution · 무인증
공격 흐름도 (직접 작성)

5.3 미흡점 ↔ 공격 흐름 매핑

미흡점(§4)발화 지점(§5.2)여는 프리미티브→ 다음으로 잇는 것
W1 — 가짜 status조각 .then() 호출 시점가짜 상태로 정상 조각 위장검증 없이 파싱(initializeModelChunk) 단계로 통과
W2 — 경로 소유 검사 부재참조 경로 순회(getOutlinedModel)Chunk.prototype / Function 생성자 획득내부 조회 함수(_formData.get) 치환 가능
W3 — 함수 정체 오신뢰Blob 참조 처리 중 _formData.get 호출임의 문자열 → 실행 가능 함수로 컴파일promise resolve 대기
(귀결)then 콜백 resolve컴파일된 함수 실행원격 코드 실행(RCE) 확정

06취약점 패치

ReactFlightReplyServer.js·getOutlinedModel()·커밋 7dc903c · 613~616행
let value = chunk.value; for (let i = 1; i < path.length; i++) { value = value[path[i]]; const name = path[i]; if (typeof value === 'object' && hasOwnProperty.call(value, name)) { value = value[name]; } }

경로 순회가 __proto__·constructor 같은 프로토타입 통로로 새어 나가는 것을 막기 위해서입니다. 615행이 바로 결함 ②가 성립하던 자리입니다.

어떻게취약하던 직접 접근(615행 삭제)을 지우고, 그 키가 객체 자신의 속성일 때만 값을 읽도록 hasOwnProperty 검사를 앞에 세웠습니다. __proto__처럼 프로토타입으로 올라가는 키는 소유 속성이 아니므로 이 검사에서 걸러집니다.

비고실제 배포된 커밋 7dc903c는 이 소유 검사 한 줄만 담은 게 아니라, ReactFlightReplyServer.js 한 파일만 822줄이 바뀐 더 큰 리팩터(ReactPromise 언랩·waitForReference 도입 등, ReactFlightClient와의 동기화)의 일부입니다. 위 diff는 그 중 보안상 핵심인 소유 검사 추가 구간만 발췌한 것입니다.

07대응방안

패치가 사실상 유일한 근본 해결입니다. 사용하는 라인에 맞춰 §영향받는 버전 표의 안전 버전으로 올립니다.

08왜 이게 반복되는가

문자열 경로를 따라 입력으로부터 객체를 복원하는 코드라면, 자바스크립트에서 프로토타입 체인은 늘 숨은 sink입니다. 자바 가젯 체인, 파이썬 pickle과 뿌리가 같습니다.

방어 원칙은 둘입니다. 첫째, 신뢰할 수 없는 키로 속성에 접근할 때는 소유 여부를 확인하거나 Object.create(null)·Map을 씁니다.

둘째, thenable·iterable처럼 '생김새로 판별되는 계약'은 그 자체가 신뢰 경계입니다. 'X처럼 생겼다'를 검증 없이 받아들이면 X의 능력까지 함께 받아들이는 셈이니까요.

이 한 틈은 패치됐지만, "외부 데이터를 검증 없이 자기 것으로 되살린다"는 습관은 다음 포맷에서 또 나타난다 — 그래서 우리는 CVE 번호가 아니라 이 사고방식을 기록으로 남깁니다.

참고 자료

공개된 취약점을 우리 관점에서 직접 분석·재구성한 연구 글입니다. 코드 발췌는 React 오픈소스(MIT)의 실제 소스입니다.