리액트 서버 컴포넌트 취약점

keemsebeen·2026년 1월 22일
post-thumbnail

해당 포스트는 25년 12월에 발생한 리액트 서버 컴포넌트의 데이터 직렬화 과정에서 발생한 취약점에 대한 글입니다.

왜 이런 일이 발생했을까요?

RSC는 서버에서 렌더링된 결과를 클라이언트로 보낼 때, 단순한 JSON이 아닌 리액트 Flight Protocol이라는 리액트만의 특수한 직렬화 형식을 사용합니다. 문제는 바로 이 프로토콜의 설계와 구현 과정에서 발생했습니다.

React Flight Protocol이란?

Flight Protocol은 리액트가 서버와 클라이언트 간에 컴포넌트 트리와 데이터를 전송하기 위해 만든 바이너리 프로토콜입니다. JSON으로는 표현할 수 없는 복잡한 자바스크립트 타입들을 전송하기 위해 설계되었습니다.

왜 JSON만으로는 부족한가?

// JSON으로 표현 불가능한 것들
const serverData = {
  onClick: () => updateUser(),        // ❌ 함수
  promise: fetchData(),               // ❌ Promise
  date: new Date(),                   // ⚠️ 문자열로 변환됨
  undefined: undefined,               // ❌ 사라짐
  symbol: Symbol('id'),               // ❌ 사라짐
  bigint: 9007199254740991n,         // ❌ 에러
};

JSON.stringify(serverData);
// 결과: {"date":"2024-01-22T..."} - 대부분 손실됨

RSC는 서버에서 클라이언트 컴포넌트로 함수를 전달하거나, Promise를 스트리밍하거나, 복잡한 객체 참조를 유지해야 하기 때문에 Flight Protocol이 필수적입니다.

발생 원인 3가지

  1. 복잡한 객체의 직렬화 허점
    리액트는 서버에서 만든 복잡한 데이터 구조(함수, 프로미스, 심볼 등)를 클라이언트가 이해할 수 있는 문자열 형태로 변환합니다. 이때 서버가 클라이언트로부터 받은 입력을 검증 없이 다시 직렬화 과정에 포함시키면서, 공격자가 "이 데이터는 단순 문자열이 아니라 서버 내부의 중요한 객체야"라고 속일 수 있는 틈이 생겼습니다.
  2. 프로토콜 해석의 모호함
    RSC 프로토콜은 $@ 같은 특수 기호를 사용해 데이터의 타입을 구분합니다. 공격자는 이 제어 문자를 조작하여 서버가 데이터를 잘못 해석하게 유도했습니다. 예를 들어, 서버가 특정 데이터를 "안전한 텍스트"로 처리해야 하는데, 공격자의 조작으로 인해 "실행 가능한 참조값"으로 오인하게 만든 것입니다.
// Flight Protocol의 참조 해석
"J0:{"user":"$1"}"  // 1번 청크를 참조
"J1:{"name":"Alice"}"

<ServerComponent userId="$1" />

공격자가 props에 "$1"을 주입하면 서버는 이를 청크 참조로 해석하게 됩니다.

  1. 클라이언트와 서버 간의 신뢰 경계 붕괴
    보안의 기본 원칙은 사용자의 입력은 절대 믿지 않는다입니다. 하지만 RSC의 초기 구현체에서는 서버 컴포넌트로 전달되는 인자(Props) 중 일부가 충분한 검증(Sanitization) 없이 직렬화 파이프라인에 태워졌고, 이것이 서버 내부 로직을 오염시키는 결과로 이어졌습니다.
// 서버 내부의 참조 테이블 (개념적 구조)
const flightReferences = new Map([
  ['0', { user: 'Alice' }],
  ['1', clientComponentModule],
  ['2', serverFunctionReference],
  
  // 내부 전용 참조 (노출되면 안됨)
  ['__DB_POOL', databaseConnection],
  ['__CONFIG', { apiKey: 'sk-secret...' }],
]);

// 취약한 참조 해석 함수
function resolveReference(ref) {
  const id = ref.slice(1); // "$1" → "1"
  return flightReferences.get(id); // ⚠️ 검증 없음!
}

실제 발생한 문제들

1. 객체 위조 (CVE-2025-55182)

서버에서 클라이언트로 데이터를 전달할 때 사용하는 바이너리 프로토콜(Flight Protocol)의 허점을 이용해, 공격자가 서버의 내부 객체나 민감한 정보에 접근할 수 있게 만드는 원격 코드 실행(RCE) 가능성 또는 정보 유출 문제입니다.

async function ServerComponent({ userId }) {
  const user = await db.users.findById(userId);
  
  return <ClientComponent data={user} />;
}

정상 흐름

userId="123" → DB 조회 → 사용자 데이터 반환

공격 흐름

공격자가 악의적으로 userId=$__DB_POOL 다음과 같은 값으로 조작하게 되면 직렬화 과정에서 내부 참조로 해석하여 서버의 내부 데이터가 노출되는 문제가 발생합니다.

// 공격자가 시도할 수 있는 값들
userId="$__CONFIG"          // API 키, 환경 설정 탈취
userId="$__SESSION_STORE"   // 다른 사용자 세션 탈취
userId="$__ADMIN_FUNCTION"  // 관리자 함수 참조 획득

2. 서비스 거부 공격 (DoS) CVE-2025-55184, CVE-2025-67779

이 취약점은 서버를 '무한 루프'의 늪에 빠뜨리는 공격입니다.

원인

리액트 서버 컴포넌트는 클라이언트에서 보낸 데이터를 서버에서 읽어 들일 때, 데이터 간의 참조 관계를 해석합니다. 공격자가 "A는 B를 참조하고, B는 다시 A를 참조하라"는 식으로 꼬여있는 악성 데이터를 보내면, 서버는 이를 해석하려다 끝없는 루프에 빠집니다.

결과

서버의 CPU 점유율이 100%로 치솟으며 실제 사용자들의 요청을 처리하지 못하게 됩니다(먹통 상태).

특이사항

패치가 한 번 나왔으나 완벽하지 않아(CVE-2025-67779) 추가 패치가 이어졌을 만큼 교묘한 공격이었습니다.

3. 소스 코드 노출 취약점 CVE-2025-55183

이건 서버에 숨겨져 있어야 할 함수의 내부 로직(코드)이 밖으로 새어 나가는 문제입니다.

원인

서버 함수(Server Functions)를 호출할 때 발생하는 에러 메시지나 특정 처리 과정에서, 서버가 실행 중인 함수의 소스 코드를 문자열 형태로 클라이언트에게 반환해 버리는 허점이 발견되었습니다.

위험성

만약 개발자가 서버 함수 내부에 실수로 API 키, DB 접속 비밀번호, 혹은 내부 비즈니스 로직을 주석이나 하드코딩된 변수로 남겨두었다면, 공격자가 이를 그대로 읽을 수 있습니다.

한계

다행히 실행 중인 환경 변수(process.env) 자체를 탈취해가는 것은 아니지만, 코드 그 자체가 자산인 기업 입장에서는 매우 치명적입니다.

export async function serverFunction(name) {
  const connect = db.createConnection('SECRET KEY');
  const user = await connect.createUser(name); 

  return {
   id: user.id,
   message: `Hello, ${name}!` 
  }}

공격자는 다음과 같은 정보를 유출할 수 있다고 합니다.

0:{"a":"$@1","f":"","b":"Wy43RxUKdxmr5iuBzJ1pN"}
1:{"id":"tva1sfodwq","message":"Hello, async function(a){console.log(\"serverFunction\");let b=i.createConnection(\"SECRET KEY\");return{id:(await b.createUser(a)).id,message:`Hello, ${a}!`}}!"}

참고
공식문서-블로그

profile
프론트엔드 개발자 김세빈입니다. 👩🏻‍💻

0개의 댓글