Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save casamia918/d66f8a265ccc8ec358beffab20ed4a33 to your computer and use it in GitHub Desktop.

Select an option

Save casamia918/d66f8a265ccc8ec358beffab20ed4a33 to your computer and use it in GitHub Desktop.

React2Shell 사건으로 본 Partial Rendering 을 위한 웹 표준의 공백

2025년 12월 3일, React 생태계를 뒤흔든 보안 취약점이 공개되었습니다. CVSS 10.0점 만점을 받은 CVE-2025-55182, 일명 "React2Shell"입니다. 이 취약점은 단순한 버그가 아닙니다. 현대 웹 개발이 직면한 근본적인 구조적 문제를 드러낸 사건이라고 볼 수 있습니다.

이 글에서는 React2Shell이 왜 발생했는지, 그리고 이것이 웹 표준의 부재와 어떤 관련이 있는지 살펴보고자 합니다.


무슨 일이 있었나

CVE-2025-55182 (CVSS 10.0)

12월 3일 공개된 첫 번째 취약점은 React Server Components (RSC)의 Flight 프로토콜에서 발생한 원격 코드 실행(RCE) 취약점입니다.

핵심 문제는 안전하지 않은 역직렬화(insecure deserialization) 였습니다. 인증 없이 HTTP 요청 하나만으로 서버를 완전히 장악할 수 있었습니다. create-next-app으로 만든 기본 Next.js 앱조차 취약했습니다.

공개된 지 수 시간 만에 중국 계열 위협 그룹을 포함한 여러 공격자들이 실제 공격에 나섰고, 이는 실제 프로덕션 서비스에서 유효한 것으로 확인되었습니다.

CVE-2025-55184 & CVE-2025-55183 (CVSS 7.5 & 5.3)

며칠 뒤인 12월 11일, 보안 연구자들이 첫 패치를 분석하던 중 추가 취약점 2개를 발견했습니다.

  • CVE-2025-55184: 서비스 거부 공격(DoS). 무한 Promise 재귀를 통해 Node.js 서버를 정지시킬 수 있었습니다.
  • CVE-2025-55183: 소스 코드 노출. Server Function의 .toString() 호출로 하드코딩된 API 키, 시크릿이 유출될 수 있었습니다.

초기 패치가 불완전하여 CVE-2025-67779라는 추가 패치까지 나왔습니다.


왜 이런 일이 발생했나

문득 이런 생각이 들었습니다.

"React는 원래 브라우저에서 UI를 그리는 라이브러리였는데, 서버까지 손을 뻗치면서 뭔가 잘못된 것 아닐까?"

HTTP 표준을 무시한 자체 프로토콜

React Server Components는 Flight라는 자체 프로토콜을 만들었습니다. 표준 HTTP/REST API가 아닌, React만의 직렬화 포맷으로 서버와 클라이언트가 통신합니다.

전통적인 HTTP 방식은 이렇습니다:

Client → [HTTP Request] → Server
Server → [JSON/HTML] → Client

✓ 명확한 경계
✓ 검증된 포맷  
✓ 보안 best practices 존재

하지만 Flight Protocol은:

Client → [React Serialized Payload] → Server
Server → [React Serialized Payload] → Client

✗ 커스텀 직렬화 포맷
✗ JavaScript 객체를 그대로 전송
✗ 함수 참조, 클로저까지 직렬화

경계를 흐린 "마법"

React 팀이 추구한 것은 개발자 경험(DX)의 극대화였습니다:

'use server'
async function updateUser(formData) {
  await db.update(formData);
}

// 클라이언트에서 "마치 로컬 함수처럼" 호출
<form action={updateUser}>

서버와 클라이언트의 경계가 투명하게 보이도록 만든 것입니다. 멋진 시도였지만, 여기에 함정이 있었습니다.

보안의 기본 원칙은 "경계를 명확히 하고, 신뢰할 수 없는 입력을 철저히 검증하라" 입니다.

React는 이 경계를 의도적으로 숨겼고, 그 결과 개발자도 React 자체도 경계에서의 검증을 소홀히 했습니다.

역사는 반복된다

이런 패턴은 처음이 아닙니다:

  1. Java RMI: "원격 객체를 로컬처럼 호출" → 무수한 역직렬화 취약점 → 대부분의 엔터프라이즈에서 사용 중단
  2. PHP의 unserialize(): 객체 직렬화를 HTTP로 전송 → RCE 취약점 다발 → 사용 금지 권고
  3. SOAP/XML-RPC: 복잡한 타입 시스템과 직렬화 → 보안 문제 → REST/JSON으로 대체

"개발자 경험"과 "보안" 사이의 트레이드오프에서, 높은 추상화가 편리함을 주지만 동시에 보안 경계를 흐리게 만듭니다.


근본 원인: HTTP는 너무 오래되었다

하지만 여기서 한 걸음 더 나아가 생각해봐야 합니다.

HTTP 프로토콜 자체가 현대 웹 애플리케이션의 복잡함을 따라가기에는 너무 낡은 것 아닐까요?

HTTP의 한계

HTTP는 1991년에 만들어진 프로토콜입니다. 그 본질은:

  • Resource 중심 (URL = Resource identifier)
  • Request/Response 모델
  • Stateless

하지만 현대 웹 앱이 필요한 것은:

  • Component 중심 (URL ≠ Component)
  • Streaming/Incremental updates
  • Stateful interactions

HTTP는 "문서 전송 프로토콜"이지 "애플리케이션 프로토콜"이 아닙니다.

Partial Rendering의 현실

사용자가 댓글 하나를 추가했을 때를 생각해봅시다:

// 방법 1: 전체 페이지 리로드
window.location.reload(); // ← 2000년대 방식

// 방법 2: JSON으로 받아서 클라이언트가 렌더링  
const comment = await fetch('/api/comments', {method: 'POST'});
setComments([...comments, comment]); // ← React의 방식

// 방법 3: HTML 조각을 받아서 삽입
const html = await fetch('/api/comments/latest');
container.innerHTML = html; // ← htmx 방식

// 방법 4: RSC - "마법"
// ← React의 시도, 하지만 보안 문제

모두 임시방편(workaround)입니다.

각자도생하는 프레임워크들

현재 각 프레임워크가 제각각 같은 문제를 다르게 해결하고 있습니다:

  • React: RSC (Flight Protocol)
  • Phoenix: LiveView (WebSocket + diffing)
  • Laravel: Livewire (Ajax + Morphdom)
  • Hotwire: Turbo Streams
  • htmx: HTML over the wire
  • Qwik: Resumability

모두가 자체 직렬화 포맷을 발명하고, 상호운용성이 없으며, 보안 모델이 제각각입니다.

React의 사건이 보여준 것은, 표준의 부재가 각 프레임워크로 하여금 위험한 자체 프로토콜을 만들도록 유도한다는 것입니다.


Partial Rendering 웹 표준이 필요하다

저는 Partial Rendering에 대한 웹 표준 정의가 필요하다고 생각합니다.

표준이 다뤄야 할 것들

1. Component Addressing
   - URL만으로는 부족
   - 페이지 내 특정 컴포넌트를 식별하는 방법
   
2. Incremental Transfer
   - 전체가 아닌 변경된 부분만 전송
   - Streaming 지원
   
3. State Synchronization
   - 서버와 클라이언트의 상태 동기화
   - Optimistic updates 처리
   
4. Security Model
   - 어떤 컴포넌트가 서버 권한으로 실행되는가?
   - 클라이언트 입력을 어떻게 검증하는가?

가능한 접근 방식

HTTP 확장:

PATCH /page HTTP/2
Content-Type: application/component-update+json
X-Component-Path: /comments/new

{
  "component": "Comment",
  "props": { "text": "Hello" },
  "action": "append"
}

또는 HTML 확장:

<!-- 서버가 응답 -->
<template component="Comment" action="append" target="#comments">
  <div class="comment">New comment</div>
</template>

표준화의 딜레마

하지만 표준을 만드는 것은 쉽지 않습니다:

  1. 누가 정의할 것인가? W3C/WHATWG는 느리고, 프레임워크 커뮤니티는 파편화되어 있습니다.

  2. 추상화 수준의 문제: 너무 low-level이면 사용하기 어렵고, 너무 high-level이면 유연성이 부족합니다.

  3. 기존 인프라와의 충돌: CDN, proxy, cache, WAF, IDS는 새 프로토콜을 이해할까요?


결론

React2Shell 사건은 단순한 구현 버그가 아닙니다. 이것은:

  1. HTTP 표준이 현대 웹 앱의 요구사항을 따라가지 못하고
  2. 각 프레임워크가 자체 해결책을 만들면서
  3. 보안 원칙을 무시한 결과

입니다.

React의 실수는 표준이 없어서가 아니라, 표준 없이도 지킬 수 있는 원칙(명시적 경계, 입력 검증)을 무시했기 때문입니다. 하지만 동시에, 표준이 있었다면 React도 위험한 자체 프로토콜을 만들지 않았을 것입니다.

Partial Rendering에 대한 웹 표준이 필요합니다.

표준화에는 시간이 걸리고 완벽하지 않겠지만, 각 프레임워크가 제각각 위험한 프로토콜을 발명하는 것보다는 낫습니다. TypeScript가 공식 표준은 아니지만 사실상 표준이 되었듯이, 주요 프레임워크들이 협력하여 공통 교집합을 찾고, 이를 브라우저 벤더에 제안하는 방식도 가능할 것입니다.

그 전까지는, 개발자들이 "마법" 같은 편리함보다 "명확한 경계"와 "입력 검증"이라는 기본 원칙을 지키는 것이 중요합니다.


참고자료


이 글은 Claude와의 대화를 기반으로 하여 작성되었습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment