<Demopeu/>

React2Shell 긴급 대응 권고에 대하여

React2Shell 긴급 대응 권고에 대하여

devreactsecurityRSCprototype-pollution

React 서버 컴포넌트에서 심각한 보안 취약점 발견 링크

최근, 엄청나게 큰 보안 문제가 터졌다. 취약한 RSC를 사용하는 서버는 공격자가 인증 없이 원격으로 명령어를 실행할 수 있다는 것이다.

React2Shell(CVE-2025-55182) 취약점 완전 분석 링크

을 읽고 내 생각을 정리해 보았다.

보안점 추가

이 글을 작성하고 이해하던 중에 또 보안 문제가 터지고 있다. 심각한 CVE 발견 이후에 후속 취약점이 드러나는 것은 흔한 일이라곤 하지만, 이 문제 또한 점수가 많이 높다. 참고하도록 하자.

React 서버 컴포넌트에서의 서비스 거부 공격 및 소스 코드 노출 링크

📌 기술적 배경

공격자가 인증 절차 없이 인터넷을 통해 서비스에 접근하는 것만으로 임의 명령 실행 가능 -> 시스템 완전 장악까지 가능

1. 배경

React의 시대가 이어 오던 중 CSR(client side rendering)의 한계가 드러나면서 서버에서 렌더링을 처리(SSR, server side rendering)하여 CSR과 합쳐지는 시도가 일어났다. 이때, React에서는 RSC(React Server Components)라는 개념을 제시했다.

CSR의 한계

  • 초기 로딩 시간이 길어짐
  • 검색 엔진 최적화(SEO)가 어려움
  • 보안 이슈

RSC를 이용할 때, 서버로 보내는 데이터를 직렬화하기 위해 직렬화 포맷인 JSON는 복잡한 React component를 다루기엔 부적절했다. 그래서 Flight Protocol이라는 독자적인 프로토콜 및 직렬화 포맷을 사용했다.

2. JS의 동작 원리와 프로토타입 오염

JavaScript는 Class처럼 복제되는 것이 아니라, 자신이 참조하는 또 하나의 객체를 기반으로 동작을 확장해 나가는 Prototype 기반의 객체지향 프로그래밍 언어다.

Prototype 언어 빠르게 이해하기

JS에서 배열은 Array.prototype을 프로토타입으로 삼는데 배열의 push,pop과 같은 메소드는 Array.prototype에 구현되어 있다. 이걸 참조해서 사용하는 것.

이러한 특성 때문에 어떤 경로로든 프로토타입 객체에 property를 설정하면 이후에 생성되는 객체들에게 영향을 줄 수 있다. 이렇게 객체의 prototype을 오염시키거나 불건전하게 접근하는 행위를 **프로토타입 오염(Prototype Pollution)**이라고 한다.

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

Object.prototype.foo = 'polluted'; // Object의 프로토타입 객체

const obj2 = {};
console.log(obj2.foo); // polluted
console.log(obj2.hasOwnProperty('foo')); // false (자신의 property가 아니기 때문)

📌 원인 분석 및 결과

React GitHub의 7dc903c 커밋을 살펴보면 packages/react-server/src/ReactFlightReplyServer.js 파일을 보면 문제를 알 수 있다.

간단히 보자면,

Flight Protocol 요청 발생 시 초기 청크 초기화 -> 요청 데이터로부터 Model 복원 -> 문자열 데이터로부터 Model 생성(역직렬화) -> 역직렬화 과정 중 발생하는 참조 처리

로 이루어져 있다.

문제 1 : 역직렬화 함수의 문제

case '@': {
  // Promise
  const id = parseInt(value.slice(2), 16);
  const chunk = getChunk(response, id);
  return chunk;
}

역직렬화 과정에서 @로 시작하는 문자열에 대해서는 Chunk Promise 자체를 받아서 반환하고 있다. 여기서 참조를 얻을 수 있다.

문제 2 : 참조 처리에서의 보안 문제

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]];
      }
      return map(response, value);
  /* (후략) */

여기서, hasOwnProperty을 사용하지 않고 그냥 넘어가기 때문에 __proto__을 통해 프로토타입 오염이 가능하다.

예를 들어 참조 표현식이 $1:__proto__:aaa와 같은 경우, 1번 청크의 프로토타입 객체의 aaa라는 멤버를 참조하게 된다.

이 때 1번 Chunk가 바라보는 프로토타입 객체가 모든 청크를 처리하는 Chunk Promise 객체(최초의 부모)라면 모든 Chunk를 처리하는 프로토타입 객체에 접근이 가능하다는 소리다.

결과 및 내가 뭘 조심하면 될까?

공격자는 이제 모든 청크를 처리하는 프로토타입 객체에 접근할 수 있기 때문에 초기화 값을 건드려 임의의 JSON object를 역직렬화 할 수 있다.

임의의 함수 호출 또한 할 수 있게 되는데, 응답 또한 공격자가 조작 가능하기 때문에 사용자 컴퓨터에서 실행할 수 있는 JS 함수도 만들 수 있다.

그렇다면 나는 무엇을 해야 할까?

  1. React/Next.js를 항상 최신 보안 패치 버전으로 유지하고, React 공식 블로그/보안 공지의 가이드를 그대로 따라 적용한다.
  2. RSC/Flight 요청을 처리하는 엔드포인트가 외부에 불필요하게 노출되지 않도록 하고, 노출이 필요하다면 반드시 인증/권한 체크가 먼저 되도록 구성한다.
  3. DoS류(서비스 거부) 이슈도 같이 따라오는 경우가 많으니, 해당 경로에 rate limit/타임아웃/요청 크기 제한을 적용하고 모니터링을 붙인다.

📌 나의 생각

나는 이번 공격을 JS의 프로토타입 체인 특성 때문에 생긴 오류고, 그걸 그냥 너무 널널하게 받아들인 역직렬화 쪽 문제로 이해했다.
최근 다녔던 부트캠프에 강사님께서 나에게 한 이야기가 있다. "왜 React, Next.js 동작 원리를 이해하려고 하냐. 그냥 쓰면 된다." 현업에서 "일단 만들고 보자"는 태도가 속도를 만들기도 하지만, 원리를 모르면 오류의 의미를 판단하기도 어렵고, 패치의 범위나 영향도를 스스로 검증하기도 어렵다. "그냥 쓰면 된다"는 말은 결국 **'검증과 책임을 프레임워크(또는 운)에 맡기라'**라고 생각한다. 나는 단순히 웹 퍼블리셔, 웹 프론트 개발을 하고 싶지 않다. 왜 그렇게 동작하는지 이해하고 더 안전하고 튼튼한 선택을 할 수 있는 개발자가 되고 싶다. 이번 기회를 통해서 JS의 동작 원리와 React 동작 원리에 다가간 것 같아 기쁘다.