React2Shell 분석: React Flight의 치명적인 RCE에 대한 심층 탐구

React2Shell (CVE-2025-55182)의 발견은 호기심 기반 보안 연구의 교과서와도 같습니다. 단순히 문서화되지 않은 프로토콜을 이해하고자 하는 작은 욕구에서 시작된 것이 수백만 개의 웹사이트에 영향을 미치는 React Server Components (RSC)와 Server Functions를 대상으로 하는 치명적인 원격 코드 실행(RCE) 취약점으로 발전했습니다.

이 글에서는 초기 호기심 단계부터 최종 익스플로잇 체인에 이르기까지의 기술적 과정을 상세히 분석하고, 문서화되지 않은 프로토콜의 위험성과 현대 JavaScript 프레임워크에서 타입 안전성에 대한 착각을 강조합니다.

Flight 프로토콜의 미스터리

React2Shell을 이해하려면 먼저 Next.js와 React가 Server Components와 Server Functions 간의 클라이언트와 서버 통신을 위해 사용하는 독점 프로토콜인 "Flight"를 알아야 합니다.

표준 JSON와 달리 Flight는 Date, BigInt, Map, 순환 참조와 같은 복잡한 JavaScript 객체를 전송할 수 있습니다. 이는 메시지를 "chunks"로 나누고 특수한 $ 구문을 사용해 타입과 참조를 표시함으로써 구현됩니다. 예를 들어 $D는 날짜를, $x는 다른 chunk를 가리킵니다.

보안 연구자들에게 가장 큰 경고 신호는 문서가 없다는 점이었습니다. 이 프로토콜은 사실상 "블랙 박스"였으며, 개발자와 보안 감사자가 공격 표면을 파악하기 어려웠습니다. Hacker News의 한 댓글자는 이와 같은 사양 부재가 취약점이 공개된 후 침해 지표(IoC)를 식별하는 것을 크게 어렵게 만들었다고 지적했습니다.

첫 번째 돌파구: 프로토타입 오염 및 타입 강제 변환

이 취약점은 Flight가 속성 참조를 처리하는 방식에 대한 "명백한 안전 검사 누락"에서 시작되었습니다. 연구자는 Flight가 객체 자체뿐 아니라 그 프로토타입의 속성까지도 참조할 수 있음을 발견했습니다.

이는 타입 강제 변환 공격의 문을 열어줍니다. 많은 Next.js 애플리케이션에서 개발자는 TypeScript를 사용해 Server Functions의 타입을 정의하지만, TypeScript는 빌드 시점 도구이며 런타임에서 타입을 강제하지 않습니다.

예를 들어 다음과 같은 함수를 고려해 보세요:

async function sayHello(name: string): string {
    'use server'
    return 'Hello, ' + name + '!'
}

개발자는 string을 반환할 것으로 기대하지만, 공격자는 Flight를 이용해 커스텀 객체를 보낼 수 있습니다. 만약 공격자가 해당 객체의 toString 속성에 악성 함수를 배치하면, 서버는 문자열 연결 과정에서 암묵적으로 그 함수를 호출하게 되어 예기치 않은 코드 실행이 발생합니다.

RCE로 가는 경로: "Thenable" 악용

타입 강제 변환이 강력한 도구이지만, 완전한 RCE를 달성하려면 더 복잡한 체인이 필요합니다. 돌파구는 React가 .then() 메서드를 구현해 Promise와 유사한 동작을 하는 "thenable"을 어떻게 처리하는지를 발견한 데서 나왔습니다.

JavaScript에서 await 키워드는 관대합니다; thenable을 await하면 자동으로 .then() 메서드를 호출합니다. 연구자는 thenable을 포함한 정교한 Flight 페이로드를 전송함으로써 await decodeReply(...) 호출 중에 서버가 공격자가 제공한 함수를 실행하도록 강제할 수 있음을 발견했습니다.

이 기능을 통해 연구자는 여러 함수 호출을 연쇄시켜, 서버 메모리 내에서 임의의 함수를 실행할 수 있는 원시적인 메커니즘을 만들었습니다.

최종 익스플로잇: React 내부 위조

RCE로 가는 최종 단계는 React 내부의 Chunk 객체를 표적으로 삼는 것이었습니다. $@x 구문을 사용해 chunk의 프라미스를 생성함으로써, 연구자는 React가 정식 React chunk가 아닌 공격자가 제어하는 객체에 대해 Chunk.prototype.then을 실행하도록 강제할 수 있었습니다.

이를 통해 연구자는 Chunk의 내부 상태를 위조할 수 있었으며, 여기에는 서버 매니페스트(React에게 어떤 Server Function ID가 어떤 모듈 및 함수와 연결되는지를 알려주는 맵)도 포함됩니다.

초기에 연구자는 서버 매니페스트를 재정의해 ID를 Node.js의 child_process.exec에 매핑하려 했지만, Webpack 모듈 시스템에서 장애에 부딪혔습니다. 최종적으로 더 정교한 PoC는 Chunk 코드 내에서 React가 제어 가능한 인자를 사용해 함수를 호출하는 지점(특히 업로드된 파일 블롭을 위한 $Bx 코드)을 찾아 그곳에 악성 함수를 심어 실행하도록 했습니다.

교훈 및 산업적 영향

React2Shell은 클라이언트와 서버 코드를 근본적으로 안전하지 않은 방식으로 혼합했기 때문에 치명적인 취약점이었습니다. Hacker News의 한 비평가가 지적했듯이, 신뢰된 주체와 신뢰되지 않은 주체 사이에서 복잡한 객체를 직렬화하기 위해 완전히 새로운 프로토콜을 만드는 것은 고위험 설계 선택입니다.

개발자를 위한 주요 시사점:

  • 런타임 타입을 절대 신뢰하지 말 것: TypeScript 주석은 런타임 검증이 아닙니다. Zod나 Joi와 같은 라이브러리를 사용해 사용자 입력을 명시적으로 검증하세요.
  • 문서화되지 않은 프로토콜을 피할 것: 독점적인 "보안은 은폐에 있다"식 프로토콜은 종종 공격자가 찾기 쉽고 방어자가 수정하기 어려운 치명적인 결함을 숨깁니다.
  • 버그 바운티 프로그램을 도입할 것: 이 취약점이 17시간 만에 신속히 분류·해결된 사례는 전문 보안 연구자의 가치를 보여줍니다. 커뮤니티가 언급했듯이, 바운티 프로그램을 운영하면 연구자가 공격자보다 먼저 연락하게 됩니다.

익스플로잇이 복잡함에도 불구하고 그 영향은 파괴적이었습니다. Meta와 Vercel 팀은 Cloudflare와 같은 WAF 제공업체와 협력해 공개 권고(CVE-2025-55182) 발표 전에 방어책을 구현하기 위해 24시간 내내 작업했으며, 대규모 재앙을 방지했습니다.

Sources