본문 바로가기
FrontEnd/React.js

React Server Components CVE-2025-55182 취약점 분석

by 위그든씨 2025. 12. 5.

2025년 12월 3일에 공개된 CVE-2025-55182(CVSS 10.0)는 React Server Components에서 발견된 critical한 인증되지 않은 원격 코드 실행(RCE) 취약점입니다. 이 취약점은 "React2Shell"이라는 별칭으로 불리며, Log4Shell과 유사한 수준의 심각성과 광범위한 영향을 미칩니다.

//선 요약

React 공식 문서의 인용:

"React Server Functions allow a client to call a function on a server. React translates requests on the client into HTTP requests which are forwarded to a server."

취약점이 발생하는 이유:

클라이언트가 Server Function을 호출
React가 이를 HTTP 요청으로 직렬화
서버가 HTTP 요청을 역직렬화할 때 requireModule 함수 사용
이 과정에서 프로토타입 체인 접근 허용 → RCE

따라서 Server Function 자체는 직접적인 공격 벡터가 아니지만, RSC를 지원하는 환경이면 내부적으로 이 취약한 역직렬화 코드가 포함되어 있어서 여전히 취약합니다.
React 팀의 경고:

"Even if your app does not implement any React Server Function endpoints, it may still be vulnerable if your app supports React Server Components."

이는 RSC 인프라 자체에 취약한 코드가 포함되어 있기 때문입니다.


CVE-2025-55182 (React Server Components)
CVE-2025-66478 (Next.js)

 

취약점 발생 원인

이 취약점은 React Server Components의 Flight Protocol에서 발생하는 안전하지 않은 역직렬화(Unsafe Deserialization) 문제입니다.

1. React Flight Protocol의 구조

React Server Components는 서버와 클라이언트 간 통신을 위해 "Flight Protocol"을 사용합니다:

  • Server-to-Client Flow: 서버가 컴포넌트를 렌더링하고 직렬화하여 클라이언트로 전송
  • Client-to-Server Flow (Reply Flow): 클라이언트가 Server Function(서버에서만 실행되는 함수)을 호출할 때 HTTP 요청으로 변환하여 서버로 전송

취약점은 Client-to-Server Reply Flow에서 발생합니다.

// 서버에서만 실행되는 함수 (서버 액션) 
// 1. Server Component에서 Server Function 정의
// EmptyNote.jsx (Server Component)

import Button from './Button';

function EmptyNote() {
  async function createNoteAction() {
    'use server';  // ← 이것이 Server Function (Server Action)
    await db.notes.create();
  }
  
  return <Button onClick={createNoteAction} />;
}


// 2. Client Component에서 호출
// Button.jsx (Client Component)
'use client';

export default function Button({ onClick }) {
  console.log(onClick);  
  // {$$typeof: Symbol.for("react.server.reference"), $$id: 'createNoteAction'}
  // ← React가 이것을 특별한 참조로 직렬화
  
  return (
    <button onClick={() => onClick()}>
      Create Note
    </button>
  );
}

[HTTP 요청 변환 과정]

(1) 클라이언트: Server Function 참조를 HTTP POST 요청으로 직렬화

POST /_next/data/action/createNoteAction
Content-Type: text/x-component

$ACTION_ID: "module-id#export-name"

(2) 서버: HTTP 요청을 받아서 역직렬화

// 서버에서 실행되는 코드 (취약한 부분)
function requireModule(metadata) {
  let moduleExports = __webpack_require__(metadata[0]);  // 모듈 로드
  return moduleExports[metadata[2]];  // ← 취약점: 프로토타입 체인 접근
}

(3) 서버: 실제 함수 실행 후 결과 반환

//정리 

클라이언트가 함수 호출을 HTTP 요청으로 변환하여 서버로 전송
서버가 이 요청을 역직렬화하여 실제 함수 호출로 변환
양방향: Client → Server (Reply Flow)
이 역직렬화 과정에서 취약점 발생

 

2. 핵심 취약점: requireModule 함수의 프로토타입 체인 접근

문제가 되는 코드는 react-server-dom-webpack 패키지의 requireModule 함수입니다:

export function requireModule<T>(metadata: ClientReference<T>): T {
  let moduleExports = __webpack_require__(metadata[ID]);
  
  // ... (비동기 import 처리)
  
  if (metadata[NAME] === '*') {
    return moduleExports;
  }
  if (metadata[NAME] === '') {
    return moduleExports.__esModule ? moduleExports.default : moduleExports;
  }
  
  // 문제가 되는 부분
  return moduleExports[metadata[NAME]];
}

핵심 문제점:

  • 마지막 줄의 moduleExports[metadata[NAME]]에서 JavaScript의 bracket notation을 사용
  • 이 방식은 객체 자체의 속성뿐만 아니라 **프로토타입 체인(prototype chain)**까지 탐색
  • hasOwnProperty 체크가 없어서 상속된 속성까지 접근 가능

3. Prototype Pollution을 통한 RCE

공격자는 다음과 같은 방식으로 악용할 수 있습니다:

// 악의적인 HTTP 요청 예시
$ACTION_ID: "vm#runInThisContext"
// 또는
$ACTION_ID: "child_process#execSync"

공격 과정:

  1. 클라이언트가 Server Action을 호출할 때, 공격자가 조작된 요청 전송
  2. 서버가 requireModule 함수로 요청된 모듈을 로드
  3. metadata[NAME]에 .constructor 같은 프로토타입 속성을 포함
  4. JavaScript 함수의 .constructor 속성을 통해 전역 Function 생성자에 접근
  5. Function 생성자를 사용하여 임의의 코드 실행

구체적인 예시:

// 공격자가 보낸 payload
{
  moduleId: "user-profile-action",
  exportName: "constructor"  // 프로토타입 체인을 통해 Function에 접근
}

// 이후 악의적인 코드 문자열을 인자로 전달
["console.log('malicious code')"]

// 결과적으로 실행되는 것
new Function("console.log('malicious code')")()

4. Webpack 번들의 위험한 모듈 활용

많은 Node.js 애플리케이션은 정상적인 목적으로 다음과 같은 위험한 모듈을 포함합니다:

  • vm.runInThisContext - 임의의 JavaScript 코드 실행
  • child_process.execSync - 쉘 명령 실행
  • fs.readFileSync/writeFileSync - 파일 시스템 접근

이러한 모듈이 webpack 번들에 포함되어 있으면, 공격자는 이를 악용할 수 있습니다.

영향 범위

취약한 버전:

  • React 19.0, 19.1.0, 19.1.1, 19.2.0
  • Next.js 14.3.0-canary.77 이상, 15.x, 16.x
  • 기타 RSC를 사용하는 프레임워크 (React Router, Waku, RedwoodJS, Expo 등)

취약한 패키지:

  • react-server-dom-webpack
  • react-server-dom-parcel
  • react-server-dom-turbopack

통계:

  • Wiz 데이터에 따르면 클라우드 환경의 **39%**가 영향을 받음
  • React는 JavaScript 개발자의 **82%**가 사용 (2024 State of JavaScript)
  • Next.js는 130,000+ GitHub stars

왜 이렇게 Critical한가?

  1. 인증 불필요: 공격에 로그인이나 인증이 전혀 필요 없음
  2. 기본 설정에서 취약: create-next-app으로 생성한 기본 앱도 즉시 취약
  3. 100%에 가까운 성공률: Wiz의 PoC 테스트에서 거의 100% 성공
  4. 광범위한 영향: React와 Next.js의 막대한 사용자 기반
  5. Server Function 미사용 시에도 취약: RSC를 지원하기만 하면 취약

수정 사항

React 팀은 다음과 같이 수정했습니다:

  1. hasOwnProperty 체크 추가: 프로토타입 체인 접근 차단
  2. 객체 생성 및 순환 참조 처리 리팩토링: reviveModel, loadServerReference 함수 개선
  3. 에러 처리 강화: decodeReplyFromBusboy에 try-catch 블록 추가

패치된 버전:

  • React: 19.0.1, 19.1.2, 19.2.1
  • Next.js: 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7, 16.0.7

 

실제 패치 된 코드

1. 핵심 보안 패치 - requireModule 함수

// BEFORE (취약)
export function requireModule<T>(metadata: ClientReference<T>): T {
  const moduleExports = __webpack_require__(metadata[ID]);
  return moduleExports[metadata[NAME]];  
}

// AFTER (패치)
export function requireModule<T>(metadata: ClientReference<T>): T {
  const moduleExports = __webpack_require__(metadata[ID]);
  if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
    return moduleExports[metadata[NAME]];
  }
  return (undefined: any);
}

적용 파일:

  • ReactFlightClientConfigBundlerWebpack.js
  • ReactFlightClientConfigBundlerNode.js
  • ReactFlightClientConfigBundlerParcel.js
  • ReactFlightClientConfigBundlerTurbopack.js

2. ReactFlightReplyServer.js의 대규모 구조 개선

PR #35277의 설명에 따르면, #29823과 #33664에서 ReactFlightClient에 적용된 다음 개선사항들을 ReactFlightReplyServer에 동기화했습니다:

A. Deep Resolution of Cycles (깊은 순환 참조 해결)

PR #29823의 핵심 개선사항:

  • InitializingHandler 메커니즘 도입: JSON.parse는 begin/complete 단계를 제공하지 않지만, React Element의 첫 번째 자식이 항상 '$'(leaf 노드)라는 점을 이용
  • Blocked Chunk 추적 리팩토링: initializingHandler에 blocked 정보를 저장하고, 가장 가까운 Element나 root Chunk가 처리하도록 변경
  • Cyclic Chunk 개념 제거: Cyclic chunk는 별도의 상태가 아니라 단순히 eager하게 listener를 호출하는 blocked chunk로 통합

ReactFlightReplyServer에 적용된 변경사항:

// BEFORE: Cyclic chunk was separate concept
const cyclicChunk: CyclicChunk<T> = (chunk: any);
cyclicChunk.status = CYCLIC;

// AFTER: Unified as blocked chunk
const cyclicChunk: BlockedChunk<T> = (chunk: any);
cyclicChunk.status = BLOCKED;
// Eagerly invoke listeners

왜 중요한가:

  • 깊게 중첩된 객체 참조와 순환 참조를 안전하게 처리
  • 공격자가 복잡한 객체 구조로 서버를 혼란시키는 것을 방지

B. Deferred Error Handling (지연된 오류 처리)

PR #29823의 핵심 개선사항:

  • Element 경계에서 오류와 차단 처리
  • Error/Blocked된 직접 참조가 가장 가까운 Element를 Lazy로 변환
  • 직렬화 중 발생한 오류를 가장 가까운 JSX element와 연결하여 더 나은 스택 추적 제공

ReactFlightReplyServer에 적용된 변경사항:

// ErrorHandler와 BlockedHandler 추적 개선
if (handler.errored) {
  // Element를 Lazy로 변환하여 가장 가까운 Suspense/Error Boundary가 처리
  initializeLazyChunkFromModel(chunk, handler);
}

왜 중요한가:

  • 공격 페이로드가 오류를 발생시켜도 적절한 에러 바운더리에서 처리됨
  • 전체 서버 크래시를 방지

C. reviveModel 함수의 __proto__ 처리 강화

Miggo 문서에서 확인한 내용:

// BEFORE: prototype pollution 취약
function reviveModel(response, parentObj, key, value) {
  // ... 
  value[i] = parentObj;  // __proto__ 키로 Object.prototype 오염 가능
}

// AFTER: __proto__ 보호
function reviveModel(response, parentObj, key, value) {
  // ...
  if (parentObj !== void 0 || "__proto__" === i) {
    value[i] = parentObj;
  } else {
    delete value[i];  // __proto__는 삭제
  }
}

왜 중요한가:

  • 공격자가 __proto__ 키를 페이로드에 주입하여 Object.prototype을 오염시키는 것을 방지
  • 모든 객체의 프로토타입 체인 조작 차단

D. initializeModelChunk의 Symbol 기반 접근으로 변경

ejpir/CVE-2025-55182-research에서 확인한 변경사항:

// BEFORE: chunk._response를 직접 사용 (공격자가 JSON으로 위조 가능)
function initializeModelChunk(chunk) {
  value = reviveModel(
    chunk._response,  // ← 공격자의 가짜 _response 사용됨!
    ...
  );
}

// AFTER: Symbol 조회 사용 (JSON으로 위조 불가능)
function initializeModelChunk(chunk) {
  var response = chunk.reason[RESPONSE_SYMBOL];  // Symbol은 JSON에서 직렬화 불가
  value = reviveModel(response, ...);
}

왜 중요한가:

  • 공격자가 multipart form으로 가짜 chunk 객체를 주입하여 _response 객체를 위조하는 공격 차단
  • Symbol은 JSON으로 직렬화할 수 없으므로 공격자가 우회 불가능

E. getOutlinedModel 함수의 타입 체크 강화

// BEFORE: listener가 함수인지 확인 없이 호출
listener(value);

// AFTER: 타입 체크 추가
if ("function" === typeof listener) {
  listener(value);
} else {
  fulfillReference(response, listener, value);
}

왜 중요한가:

  • 공격자가 listener를 악의적인 객체로 대체하는 것을 방지
  • 예상치 못한 타입으로 인한 크래시나 악용 차단

3. decodeReplyFromBusboy 함수의 에러 처리 개선

// BEFORE: 에러가 전파되어 서버 크래시 가능
function decodeReplyFromBusboy(busboyStream, options) {
  resolveField(value, ...);  // 에러 발생 시 전파
}

// AFTER: try-catch로 감싸서 스트림 안전하게 종료
function decodeReplyFromBusboy(busboyStream, options) {
  try {
    resolveField(value, ...);
  } catch (error) {
    busboyStream.destroy(error);  // 스트림 안전하게 파괴
    return;
  }
}

추가 개선사항:

  • Base64 파일 업로드 거부 시 동기적 throw 대신 busboyStream.destroy() 사용
  • 파일 처리 완료(value.on('end')) 핸들러에도 try-catch 추가

왜 중요한가:

  • 악의적인 페이로드가 서버 크래시를 유발하는 것을 방지 (DoS 공격 차단)
  • 스트림 리소스 누수 방지

4. PR #33664의 InitializingReference 개선 (간접적 영향)

PR #35277 설명에서 #33664를 명시적으로 언급했지만, 이는 주로 debug info와 console log 순서 문제를 해결하기 위한 것이었습니다. 보안에 직접적인 영향은 적지만, 구조적 개선의 일부였습니다.


종합 정리: 패치의 전체 범위

CVE-2025-55182를 해결하기 위해 React 팀은 단일 함수만 수정한 것이 아니라 다음의 7가지 주요 영역을 개선했습니다:

  1. requireModule - hasOwnProperty 체크 (핵심 보안 패치)
  2. Deep Cycle Resolution - InitializingHandler와 Blocked Chunk 메커니즘 개선
  3. Deferred Error Handling - Element 경계 오류 처리 강화
  4. reviveModel - __proto__ 보호 (prototype pollution 방어)
  5. initializeModelChunk - Symbol 기반 참조 (가짜 Response 차단)
  6. getOutlinedModel - 타입 체크 (악의적 listener 방어)
  7. decodeReplyFromBusboy - 에러 처리 강화 (DoS 방어)

결론: 이것은 단순한 1줄 보안 패치가 아니라, ReactFlightClient에서 이미 검증된 구조적 개선사항 전체를 ReactFlightReplyServer에 동기화하여 다층 방어(defense in depth) 전략을 구현한 것입니다. hasOwnProperty 체크는 빙산의 일각이며, 실제로는 순환 참조 처리, 프로토타입 오염 방지, Symbol 기반 보안, 타입 안전성 등 여러 계층의 보호 장치가 함께 추가되었습니다.