129KB의 공백(및 재귀 루프)이 웹을 무너뜨린 방법

발행: (2025년 12월 13일 오후 01:00 GMT+9)
10 min read
원문: Dev.to

Source: Dev.to

Cover image for How 129KB of Whitespace (and a Recursive Loop) Broke the Web

React2Shell (CVE‑2025‑55182) 공개된 지 약 일주일이 지났습니다. 초기의 “모두 중단” 패닉은 대부분 가라앉았고, PagerDuty 알림도 이제는 조용해졌기를 바랍니다. 이제 연기가 걷히고, 숨을 돌리며 무슨 일이 일어났는지 살펴볼 수 있습니다.

저에게는 상황의 현실이 8개의 GCP(구글 클라우드) 이메일을 받았을 때 비로소 다가왔습니다. 평소와 다른 청구 알림이 아니라 다음과 같은 형태였습니다:

New Advisory Notification
Dear Google Cloud customer,
You’ve received an important Google Cloud notification affecting your resource…
Notification Title: Important Security Information Regarding React & Next.js Vulnerability (CVE‑2025‑55182)

클라우드 제공업체가 “Advisory Notification” 이메일을 대량으로 보내면서 자바스크립트 프레임워크를 언급한다면, 이는 단순 버그가 아니라 사건이라는 뜻입니다!(event!)

How We Got Here

익스플로잇을 이해하려면 아키텍처를 살펴봐야 합니다. 수년간 우리는 클라이언트와 서버 사이의 “시스템 없는” 통합을 추구했습니다. React Server Components (RSC) 가 백엔드에서 직접 데이터를 가져와 프론트엔드에 스트리밍하도록 만들고 싶었습니다.

하지만 여기서 충분히 이야기되지 않는 트레이드‑오프가 있습니다: 신뢰 경계(Trust Boundaries).

과거(정확히는 12개월 전)에는 주로 JSON을 주고받았습니다. JSON은 단순히 데이터일 뿐이라 안전했습니다. 그러나 RSC는 “실행 컨텍스트”(Promise, Symbol, Server Actions 등)를 전달해야 했습니다. JSON으로는 이를 표현할 수 없었기에 React는 Flight 프로토콜을 만들었습니다.

The Fatal Flaw in Flight

취약점은 react-server-dom-* 패키지가 Flight 프로토콜을 처리하는 방식에 있습니다. 설계상 Flight는 서버가 클라이언트가 보낸 복잡한 객체를 역직렬화(deserialize)하도록 허용합니다.

보안 역사를 공부해 본 사람이라면 “deserialize”라는 단어에 몸이 떨릴 겁니다. Java(Struts), PHP, Python 모두 여기서 치명적인 실패를 겪었습니다. React2Shell은 자바스크립트도 예외가 아니라는 것을 증명했습니다.

이 취약점을 통해 인증되지 않은 공격자는 특수하게 조작된 HTTP 요청을 보내 “thenable”(Promise‑like 객체) 를 서버에 전달할 수 있었습니다. React 내부 로직은 이 악의적인 객체를 적극적으로 “resolve”하려 하면서, 공격자는 실행 흐름을 가로채고 임의 코드를 실행할 수 있었습니다.

The WAF Bypass (Why the Email Came Too Late)

이번 주 가장 짜증나는 부분 중 하나는 우리의 보안 방어가 무너지는 모습을 보는 것이었습니다. 우리는 웹 애플리케이션 방화벽(WAF)이 이를 차단할 것이라 기대했지만, 그렇지 않았습니다.

공격자들은 대부분의 WAF가 요청 본문의 처음 8 KB에서 128 KB만 검사하도록 최적화돼 있다는 사실을 이용했습니다. 그래서 패딩(padding) 이라는 아주 단순한 기법을 사용했습니다.

그들은 악성 페이로드 앞에 약 129 KB의 “쓰레기”(공백, 주석) 데이터를 삽입했습니다. WAF는 이 쓰레기만 검사하고 문제 없다고 판단해 요청을 Next.js 서버로 넘겼습니다. 서버는 전체 본문을 읽어 페이로드를 역직렬화하고 원격 코드 실행을 트리거했습니다.

The Second Wave: It Wasn’t Just RCE

RCE 취약점을 패치했다고 스스로를 칭찬하려는 순간, 보안 연구원(및 React 팀)은 문제가 더 깊게 파고들었다는 것을 발견했습니다. 12월 11일에 파서가 단순 코드 실행뿐 아니라 구조적 남용에도 취약하다는 사실을 알게 되었고, 이에 따라 지금 바로 알아야 할 두 개의 새로운 CVE가 등장했습니다.

1. The Infinite Loop (CVE‑2025‑55184 & CVE‑2025‑67779)

Flight 프로토콜 역직렬화기는 본질적으로 재귀적입니다—참조 안의 참조를 해결해야 하기 때문이죠. 페이로드가 특정 루프 안에서 자신을 참조하도록 만들면 Node.js 프로세스는 동기식 무한 루프에 빠집니다.

Node.js는 단일 스레드이기 때문에 이는 치명적입니다. CPU 사용량이 100 %까지 치솟고 서버가 모든 사용자에게 즉시 응답하지 않게 됩니다.

서버리스 환경(Vercel, AWS Lambda)에서는 “Denial of Wallet” 현상이 발생합니다. 공격자는 함수가 타임아웃될 때까지 계속 실행하도록 강제해 수천 개의 인스턴스를 띄우고, 실제 사용하지 않은 컴퓨팅 시간에 대한 거대한 청구서를 만들 수 있습니다.

2. The Spy in the Reflection (CVE‑2025‑55183)

이것은 좀 더 무시무시합니다. 공격자가 서버를 속여 자체 소스 코드를 노출시킬 수 있게 합니다.

Server Actions에서 인자를 toString() 하거나 암묵적으로 변환한다면, 공격자는 내부 클로저 상태를 직렬화해 클라이언트에 반환하는 특수 객체를 전달할 수 있습니다.

베스트 프랙티스대로 환경 변수(process.env.DB_PASS)를 사용한다면 대부분 안전합니다—공격자는 변수 이름만 보고 값은 알 수 없습니다. 하지만 API 키나 비밀을 코드에 하드코딩했다면 이제는 공개된 것이 됩니다.

The “Patch of the Patch”

DoS 취약점이 처음 발견됐을 때 React는 19.0.2 버전을 배포했습니다. 모두가 업데이트했고, 우리는 안전하다고 생각했습니다.

하지만 연구자들은 그 패치를 우회하는 방법을 찾아냈습니다—원형 참조에 또 다른 간접 레이어를 추가함으로써 두 번째 패치 사이클을 강제했습니다. RCE를 고쳤다고 여기서 멈췄다면, 여전히 DoS와 소스 코드 노출 취약점에 노출된 것입니다.

Where We Go From Here

지난 24 시간 안에 패치를 적용하지 않았다면, 이제는 빌린 시간을 살고 있는 겁니다. 이 취약점을 완전히 완화할 설정 변경은 없으며, 의존성을 업그레이드하는 것이 필수이고, 최종 안전 버전으로 이동해야 합니다.

Upgrade List

  • Next.js 15.x: 15.0.7+ 로 업데이트 (15.0.6에 머물지 마세요)
  • Next.js 14: 14.2.35+ 로 업데이트
  • React: 19.0.3+ (19.0.x 브랜치) 혹은 19.1.4+ 로 업데이트

핵심 단계: next build재빌드하고 애플리케이션을 재배포해야 합니다. 취약한 코드는 서버 아티팩트에 번들되어 있으므로 단순 재시작으로는 해결되지 않습니다.

The Hindsight Perspective

React2Shell과 그 “후속” 취약점들은 “Full Stack” 프레임워크에 대한 대화를 바꿀 것입니다. 우리는 개발자 편의를 위해 엄격한 관심사 분리를 포기했고, 그 대가를 치렀습니다.

RSC가 사라졌다고 생각하나요? 아니요. 하지만 Next.js 앱에서 서버‑사이드 코드가 “기본적으로 안전하다”는 가정은 이제 끝났습니다.

Back to Blog

관련 글

더 보기 »