2025년 12월 3일, React 생태계를 뒤흔든 보안 취약점이 공개되었습니다. CVSS 10.0점 만점을 받은 CVE-2025-55182, 일명 "React2Shell"입니다. 이 취약점은 단순한 버그가 아닙니다. 현대 웹 개발이 직면한 근본적인 구조적 문제를 드러낸 사건이라고 볼 수 있습니다.
이 글에서는 React2Shell이 왜 발생했는지, 그리고 이것이 웹 표준의 부재와 어떤 관련이 있는지 살펴보고자 합니다.
12월 3일 공개된 첫 번째 취약점은 React Server Components (RSC)의 Flight 프로토콜에서 발생한 원격 코드 실행(RCE) 취약점입니다.
핵심 문제는 안전하지 않은 역직렬화(insecure deserialization) 였습니다. 인증 없이 HTTP 요청 하나만으로 서버를 완전히 장악할 수 있었습니다. create-next-app으로 만든 기본 Next.js 앱조차 취약했습니다.
공개된 지 수 시간 만에 중국 계열 위협 그룹을 포함한 여러 공격자들이 실제 공격에 나섰고, 이는 실제 프로덕션 서비스에서 유효한 것으로 확인되었습니다.
며칠 뒤인 12월 11일, 보안 연구자들이 첫 패치를 분석하던 중 추가 취약점 2개를 발견했습니다.
- CVE-2025-55184: 서비스 거부 공격(DoS). 무한 Promise 재귀를 통해 Node.js 서버를 정지시킬 수 있었습니다.
- CVE-2025-55183: 소스 코드 노출. Server Function의
.toString()호출로 하드코딩된 API 키, 시크릿이 유출될 수 있었습니다.
초기 패치가 불완전하여 CVE-2025-67779라는 추가 패치까지 나왔습니다.
문득 이런 생각이 들었습니다.
"React는 원래 브라우저에서 UI를 그리는 라이브러리였는데, 서버까지 손을 뻗치면서 뭔가 잘못된 것 아닐까?"
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 자체도 경계에서의 검증을 소홀히 했습니다.
이런 패턴은 처음이 아닙니다:
- Java RMI: "원격 객체를 로컬처럼 호출" → 무수한 역직렬화 취약점 → 대부분의 엔터프라이즈에서 사용 중단
- PHP의
unserialize(): 객체 직렬화를 HTTP로 전송 → RCE 취약점 다발 → 사용 금지 권고 - SOAP/XML-RPC: 복잡한 타입 시스템과 직렬화 → 보안 문제 → REST/JSON으로 대체
"개발자 경험"과 "보안" 사이의 트레이드오프에서, 높은 추상화가 편리함을 주지만 동시에 보안 경계를 흐리게 만듭니다.
하지만 여기서 한 걸음 더 나아가 생각해봐야 합니다.
HTTP 프로토콜 자체가 현대 웹 애플리케이션의 복잡함을 따라가기에는 너무 낡은 것 아닐까요?
HTTP는 1991년에 만들어진 프로토콜입니다. 그 본질은:
- Resource 중심 (URL = Resource identifier)
- Request/Response 모델
- Stateless
하지만 현대 웹 앱이 필요한 것은:
- Component 중심 (URL ≠ Component)
- Streaming/Incremental updates
- Stateful interactions
HTTP는 "문서 전송 프로토콜"이지 "애플리케이션 프로토콜"이 아닙니다.
사용자가 댓글 하나를 추가했을 때를 생각해봅시다:
// 방법 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에 대한 웹 표준 정의가 필요하다고 생각합니다.
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>하지만 표준을 만드는 것은 쉽지 않습니다:
-
누가 정의할 것인가? W3C/WHATWG는 느리고, 프레임워크 커뮤니티는 파편화되어 있습니다.
-
추상화 수준의 문제: 너무 low-level이면 사용하기 어렵고, 너무 high-level이면 유연성이 부족합니다.
-
기존 인프라와의 충돌: CDN, proxy, cache, WAF, IDS는 새 프로토콜을 이해할까요?
React2Shell 사건은 단순한 구현 버그가 아닙니다. 이것은:
- HTTP 표준이 현대 웹 앱의 요구사항을 따라가지 못하고
- 각 프레임워크가 자체 해결책을 만들면서
- 보안 원칙을 무시한 결과
입니다.
React의 실수는 표준이 없어서가 아니라, 표준 없이도 지킬 수 있는 원칙(명시적 경계, 입력 검증)을 무시했기 때문입니다. 하지만 동시에, 표준이 있었다면 React도 위험한 자체 프로토콜을 만들지 않았을 것입니다.
Partial Rendering에 대한 웹 표준이 필요합니다.
표준화에는 시간이 걸리고 완벽하지 않겠지만, 각 프레임워크가 제각각 위험한 프로토콜을 발명하는 것보다는 낫습니다. TypeScript가 공식 표준은 아니지만 사실상 표준이 되었듯이, 주요 프레임워크들이 협력하여 공통 교집합을 찾고, 이를 브라우저 벤더에 제안하는 방식도 가능할 것입니다.
그 전까지는, 개발자들이 "마법" 같은 편리함보다 "명확한 경계"와 "입력 검증"이라는 기본 원칙을 지키는 것이 중요합니다.
- React 공식 블로그: Critical Security Vulnerability in React Server Components
- Wiz Research: React2Shell (CVE-2025-55182)
- AWS Security Blog: China-nexus cyber threat groups rapidly exploit React2Shell
- React 공식 블로그: Denial of Service and Source Code Exposure
이 글은 Claude와의 대화를 기반으로 하여 작성되었습니다.