2025년 12월 3일, React Server Components(RSC) 환경에서 서버 측 원격 코드 실행(RCE) 이 가능한 취약점 React2Shell(CVE-2025-55182) 이 공개되었습니다.
특히 CVSS 10점 만점 중 10으로 매우 Critical한 수준으로 평가되고 있으며, EPSS(악용 가능성 지수)는 12월 9일 기준 77.80%로 높은 수준입니다.
또한, KEV(미국 CISA Known Exploited Vulnerabilities) 리스트에도 공식 등록되어 실제 악용 가능성이 큰 위협으로 분류되고 있습니다.
지엔 혁신연구팀은 해당 CVE에 기술적인 관심을 가지고 심층 분석을 진행하였습니다.
1. 취약점 개요
1.1 기본 정보

※ EPSS (악용 가능성) : First.org에서 제공하는 취약점 데이터 기반의 ML 예측 지수
※ KEV(실제로 악용된 취약점): 미국 CISA가 관리하는 목록으로 실제로 악용된 취약점을 공식적으로 식별한 리스트
1.2 핵심 문제 구조
React2Shell은 다음 세 가지 설계적 결함이 동시에 작동하면서 발생한 취약점입니다.
Unsafe Deserialization: Flight 참조 문자열(reference string)에 대한 검증 부재
Prototype / Context Pollution: RSC 내부 컨텍스트 객체를 사용자 입력으로 오염 가능
Thenable 기반 자동 실행: thenable 규칙에 의해
then()이 자동 호출되면서 RCE 트리거
이 세 요소는 RSC + Flight + Server Actions 라는 새로운 렌더링/데이터 모델 위에서 자연스럽게 연결되며, 결과적으로 서버 코드 실행으로 이어집니다.
2. RSC와 Flight: 문제의 기반이 된 아키텍처
2.1 렌더링 모델의 변화
웹 렌더링은 다음과 같은 흐름으로 발전했습니다.

[그림 1] 시대별 렌더링 변화 타임라인
전통 서버 렌더링 (PHP / JSP / Rails 등)
서버에서 HTML 완성본을 렌더링하여 전송
페이지 전환마다 전체 새로고침, 상태 유지가 어렵고 실시간성 요구에 한계
jQuery + Ajax
서버 템플릿 + Ajax로 데이터만 받아와 DOM 일부 갱신
구조적 한계로 인해 대규모·복잡 UI에서는 유지보수 비용 증가
SPA / CSR (React / Angular / Vue)
서버는 빈 HTML + JS 번들, 렌더링/라우팅/상태 관리는 브라우저에서 수행
인터랙션은 향상되었으나, 초기 로딩/SEO/번들 크기 문제가 지속
SSR + CSR 하이브리드 (Next.js 등)
초기 HTML을 서버에서 SSR, 이후 Hydration으로 SPA처럼 동작
CSR의 단점을 일부 완화하지만, Hydration 비용·데이터 중복·워터폴 문제는 남음
SSG / JAMstack / ISR
빌드 시 HTML을 미리 생성(SSG)하여 CDN에서 제공
정적 콘텐츠에는 적합하나, 동적/사용자별 페이지에는 제약
RSC(React Server Components)
컴포넌트를 서버/클라이언트 컴포넌트로 분리
서버 컴포넌트는 서버에서만 실행되고 JS 번들에 포함되지 않으며, 결과는 Flight 프로토콜로 직렬화되어 클라이언트로 스트리밍
RSC는 컴포넌트를 서버 컴포넌트와 클라이언트 컴포넌트로 분리해 성능을 극대화하고, 서버에서 실행된 컴포넌트 결과를 Flight 프로토콜이라는 전용 직렬화 포맷으로 스트리밍합니다.
React2Shell은 바로 이 RSC + Flight 계층의 역직렬화 설계 취약점에서 비롯된 문제입니다.
2.2 RSC와 Flight 직렬화 포맷
RSC는 JSON으로 표현하기 어려운 다양한 구조(Promise, Blob, Server Actions) 등을 다루기 위해 Flight라는 전용 직렬화 포맷을 사용합니다.
Flight의 핵심 특징은 다음과 같습니다.
하나의 JSON 덩어리가 아니라 여러 개의 스트림 chunk로 구성
각 chunk는 고유한 ID와 직렬화 정보로 구성
클라이언트는
id → value매핑 테이블을 채우며 해석특정 문자열 토큰을 사용해 타입과 참조를 표현
예시 토큰들은 다음과 같습니다.
$D…:Date$undefined,$null: 특수 값$@id: 해당 ID의 chunk 전체를 참조 (예:$@0→ id 0인 chunk value)$Bhex: Blob/파일을 간접 참조하는 토큰 ($B1337등)"1:foo:bar": “ID=1인 chunk의 value에서.foo.bar경로를 따라가라”는 의미의 참조 문자열
즉, Flight payload는 “어떤 chunk에서 어떤 경로를 어떻게 따라갈지”를 기술하는 미니 언어에 가깝습니다.
역직렬화 단계에서는 이 토큰들을 그대로 신뢰해 내부 JS 객체 접근·Blob 조회·서버 함수 참조 등을 수행합니다.
React2Shell의 문제는 이 문자열들이 아무 검증 없이 내부 객체 접근 흐름으로 사용되었다는 것입니다.
2.3 Server Actions와 Flight 관계
React 19 / Next.js App Router에서 Server Actions(Server Functions)는 다음과 같은 흐름으로 작동합니다.
클라이언트에서
form제출 또는await someAction(formData)호출브라우저는 이를
multipart/form-data형태로 서버에 전송(일반 필드, 파일(Blob), RSC 메타데이터, Flight 참조 문자열 등이 포함)
서버의
/app/_actions등 엔드포인트 처리a. HTTP 요청 파싱 →
FormData/body생성b. Flight 역직렬화기를 호출해 “React가 이해하는 모델 구조”로 복원
c. 모델 내부에서 호출할 Server Function을 찾아 실행
취약점은 3-b, Flight 역직렬화기가 공격 입력을 그대로 신뢰하면서 발생합니다.
3. Prototype / Context Pollution, Blob 토큰, Thenable
React2Shell 취약점을 이해하기 위해서는 다음 세 가지 개념을 분리해 살펴보는 것이 도움이 됩니다.
Prototype / Prototype Pollution
Context Pollution (RSC 내부 컨텍스트 오염)
Blob 토큰 처리 및 thenable 자동 호출 규칙
3.1 Prototype과 constructor.constructor 패턴
JavaScript의 모든 객체는 프로토타입 체인을 기반으로 동작합니다.
{}→Object.prototype[]→Array.prototype
그리고 다음과 같은 체인 접근이 가능합니다.

즉, constructor.constructor는 Function 생성자에 도달하는 전형적인 primitive이며, 이 값이 실제 호출 흐름과 연결될 경우 서버 측 코드 실행(RCE) 로 이어질 수 있습니다.
3.2 Prototype Pollution vs Context Pollution

React2Shell에서는 다음과 같은 조합이 사용되었습니다.
Flight 참조 문자열을 통해
__proto__.constructor.constructor에 도달 →Function획득획득한
Function을 다시 RSC 내부 컨텍스트 객체의 필드(_formData.get,_prefix,then등)에 주입
즉, Prototype Pollution의 전형적인 경로를 이용해 얻은 primitive를 Context Pollution 형태로 React 내부에 삽입하는 구조입니다.
3.3 Blob 토큰( $B... )의 역할
Flight에서 Blob 토큰은 Blob파일에 대한 간접 참조 문자열입니다.

서버는 이를 다음과 같은 흐름으로 처리합니다.

원래 의도는 단순합니다. 모델 복원(revive) 과정에서 문자열이 $B… 패턴을 만족하면parseModelString() → resolveBlobToken()이 호출되고, 그때마다 _formData.get(_prefix + id) 호출이 발생하도록 설계되어 있습니다.

그러나 React2Shell에서는 이 세 요소가 모두 공격자 제어 하에 놓일 수 있었습니다.

3.4 Thenable과 Promise Resolution 규칙
Thenable은 “정식 Promise 인스턴스는 아니지만 then() 메서드를 가진 객체”를 의미합니다.

표준 규칙(Promise Resolution Procedure)에 따르면 동작 방식은 다음과 같습니다.
Promise.resolve(x)호출 시,x가 thenable이면x.then(resolve, reject)가 자동 호출await x역시 내부적으로Promise.resolve(x)를 호출
RSC는 이 규칙을 활용해 Flight chunk를 thenable처럼 관리합니다.
각 chunk는 개념적으로 다음과 같은 형태를 갖습니다:

React는 특정 시점에 이런 chunk를 Promise.resolve(chunk)와 유사한 방식으로 처리하므로, 표준 규칙에 의해 chunk.then(...)이 자동 호출됩니다.
그리고 이 then 내부에서 호출되는 reviveModel()이 앞서 본 것처럼 문자열/배열/객체를 재귀적으로 순회하면서 문자열이 $Bxxxx 패턴일 때
3.3에서 정의한 parseModelString() → resolveBlobToken() 경로를 통해 Blob 토큰을 처리하게 됩니다.
즉, “thenable 자동 실행 지점”과 “Blob 토큰 처리 지점”이 같은 실행 흐름 안에 묶여 있는 구조라고 볼 수 있습니다.
4. Flight 역직렬화 설계의 세 가지 실수
React2Shell은 아래 세 가지 설계 결함이 동시에 충족되면서 발생한 취약점입니다.
참조 문자열(reference string)에 대한 검증 부재
내부 컨텍스트 객체를 사용자 입력으로 덮어쓰는 구조
이 오염된 객체를 thenable 체인에 연결하는 설계
4.1 1단계 – 참조 문자열을 그대로 신뢰한 구조
Flight는 "1:foo:bar"와 같은 문자열을 “ID=1인 chunk의 value에서 .foo.bar 경로를 따라가라”는 의미로 사용한다. 문제는 이 경로에 등장하는 키에 대한 필터링이 없었다는 점이다.
공격자는 다음과 같은 문자열을 그대로 전달할 수 있습니다.

이를 처리하는 로직은 다음과 같은 형태였습니다.

그래서 단순 문자열 입력만으로 Function 생성자에 도달할 수 있게 됩니다.
4.2 2단계 – 내부 컨텍스트 객체 오염 (Context Pollution)
Flight 역직렬화기는 모델 전체를 재귀적으로 순회하며 문자열·배열·객체를 복원합니다.

React 내부에서는 다음과 같은 필드를 컨텍스트 객체 속성으로 사용한다.
_response_formData_prefixstatusthen
공격자가 JSON payload에 이 키들을 포함시키기만 하면, 역직렬화 후 컨텍스트 객체 내부 필드들은 그대로 공격자 값으로 치환됩니다.

역직렬화가 끝난 후, 내부 상태는 다음과 같이 오염됩니다.
_response: 원래 컨텍스트 대신 공격자 객체_formData: 실제 FormData가 아닌 임의 객체_prefix: 나중에 코드 일부로 사용될 문자열status,then: 비동기 흐름 제어에 사용되는 필드 역시 공격자 값으로 변경
여기에 1단계에서 획득한 Function 생성자를 _formData.get에 주입할 수 있습니다.

이제 내부 로직이 _formData.get(prefix + id)를 호출하는 시점이 코드 실행 지점이 됩니다.
4.3 3단계 – Blob 토큰 처리 + thenable 체인 자동 실행
Blob 토큰 처리 과정은 다음과 같은 형태를 가집니다.

앞서 본 것처럼, 이 함수는 reviveModel() → parseModelString() 경로에서 문자열이 $B… 패턴일 때마다 호출되도록 설계되어 있습니다.
즉, Flight 모델 안에 $B1337과 같은 토큰이 존재하면, 해당 chunk가 최종적으로 “모델로 해석되는” 시점에 _formData.get(_prefix + id) 호출이 반드시 한 번 지나가게 됩니다.

이 로직은 Flight chunk의 처리 과정, 즉 thenable 체인의 일부에서 호출됩니다.
Flight chunk는 thenable처럼 설계
React는 chunk가 특정 상태에 도달하면
Promise.resolve(chunk)처럼 처리JS 표준에 따라 thenable인 chunk는 이때
chunk.then(...)이 자동 호출then 내부에서는
reviveModel()이 실행Blob 토큰(
$Bxxxx)이 발견되면resolveBlobToken()이 호출_formData.get(_prefix + id)가 실행
이미 이전 단계에서 _formData.get이 Function으로 오염된 상태라면, 이 호출은 곧바로 Function(_prefix + ...)() 형태의 코드 실행로 이어집니다.
5. 정리: 프레임워크는 ‘믿고 쓰는 블랙박스’가 아니다
React는 오랜 기간 안정적으로 사용되었고, 많은 개발자가 React 내부 구조를 깊이 들여다보지 않아도 문제 없이 개발해왔습니다.
하지만 React2Shell은 그 믿음을 깨는 사건이었습니다.
프레임워크 내부 컨텍스트가 공격자 입력에 노출될 수 있고
자동 실행 규칙이 예상치 못한 취약점이 되고
Flight 해석기가 단 하나의 문자열로 Function 생성자로 이어진다
이 사건을 통해 크게 느낀 점:
“프레임워크도 결국 사람이 만든 코드이며, 신뢰의 경계 바깥에 둘 수 없다.”
“우리가 이해하지 못한 추상화는, 공격자에게는 선물이다.”
🔜 다음 글 (Part 3) 예고: “React2Shell 대응 이후, 기업 보안은 어디로 가야 하는가?”
여기에서는 단순 스캔을 넘어, 조직이 이번 사건을 계기로 어떤 보안 체계를 갖춰야 하는지를 설명합니다.
SBOM·VEX 기반 대응 체계가 왜 필요한지, 그리고 향후 대규모 프레임워크 취약점이 반복될 때 기업이 흔들리지 않는 대응력을 갖추는 방법을 정리합니다.