
CVE-2025-55182에 대한 개념 증명 익스플로잇으로, 안전하지 않은 Server Actions 역직렬화를 통해 React Server Components에서 인증 없이 원격 코드 실행(RCE)이 가능함을 입증하며, 상세한 재현 단계와 완화 지침을 포함합니다.
이 저장소는 React Server Components(RSC)에서 인증 없이 임의 코드 실행을 허용하는 치명적인 보안 취약점인 CVE-2025-55182의 PoC 재현을 포함합니다.
이 취약점은 React Server Components가 클라이언트 요청에서 "Server Actions"를 역직렬화하는 방식에 존재합니다. 구체적으로, requireModule 함수는 요청된 export 이름이 모듈의 직접적인 prop인지 검증하지 못했습니다. 이로 인해 공격자는 내보내진 함수의 constructor prop에 접근하여 전역 Function 생성자에 대한 참조를 얻을 수 있었고, 이를 사용해 임의 코드를 실행할 수 있습니다.
이 PoC는 최소한의 Node.js 환경을 사용하여 react-server-dom-webpack 라이브러리에서 취약점을 격리합니다. 익스플로잇이 프레임워크의 잘못된 설정이 아니라 라이브러리 자체의 버그를 입증하도록 하기 위해서입니다.
npmnpm install
[!NOTE]
package.json은 취약한 버전19.0.0으로 고정되어 있습니다.
이 스크립트는 취약한 React 런타임을 사용하여 요청을 디코딩하는 원시 HTTP 서버를 설정합니다.
# tty1
node --conditions react-server server.js
별도의 터미널에서 익스플로잇을 실행합니다. 이는 악성 Flight 페이로드를 서버로 전송합니다.
# tty2
node exploit.js id
응답에 명령 출력이 반환되는 것을 확인할 수 있습니다:
예상 출력:
Response: uid=0(root) gid=0(root) groups=0(root)
왜 취약점이 발생했나요?
ReactFlightDOMServerNode.js의 requireModule 함수는 기본적으로 클라이언트가 보낸 name을 그대로 신뢰했습니다. 해당 속성이 실제로 노출되도록 의도된 것인지 확인하지 않고 moduleExports[metadata[NAME]]를 수행했습니다. 그래서 클라이언트가 "야, 이 속성을 원해"라고 말하면 서버는 "물론이지! 여기 있어, 친구"라고 응답했습니다.
아무 속성에나 접근하게 하는 것이 왜 나쁜가요?
기본적으로 누구나 프로토타입 체인에 접근할 수 있게 되기 때문입니다. 심지어 매우 위험한 **constructor**에도 접근할 수 있습니다. 모듈이 함수를 내보낼 경우(예: module.exports = () => {}) 해당 함수의 constructor는 말 그대로 전역 Function 생성자입니다.
Function 생성자를 얻는 것이 왜 RCE로 이어지나요?
공격자가 Function 생성자를 확보하면 "Bound Server Action" 기능을 악용할 수 있습니다. 악성 JavaScript가 포함된 문자열을 여기에 바인딩하여(기본적으로 new Function("evil code")로 변환) 실행되면 서버는 공격자가 넣은 모든 코드를 실행합니다.
React가 실제로 그 악성 함수를 실행하는 이유는 무엇인가요?
Server Actions는 ID로 트리거될 수 있기 때문입니다. 공격자가 자신의 module#constructor 참조를 가리키는 Action ID가 포함된 페이로드를 만들면, React는 이를 일반 액션처럼 해석하고 실행합니다. 그 "액션"은 실제로 공격자의 악성 함수입니다.
왜 이 중 어떤 것도 검증되지 않았나요?
시스템은 Server Reference 메타데이터의 id와 name이 항상 개발자가 정의한 유효한 export를 가리킬 것이라고 가정했습니다. 요청된 prop이 실제 export인지, 프로토타입 체인에서 상속된 것이 아닌지 확인하는 hasOwnProperty와 같은 안전 검사는 없었습니다.
server.js인가?저는 React Server Components 런타임을 수동으로 구성하기 위해 원시 server.js(및 헬퍼 webpack-runtime.js)를 사용합니다. 이를 통해 다음을 수행할 수 있습니다:
module.exports = fn)s로 내보내질 때만 작동합니다. 실제 번들러는 구성에 따라 export가 래핑되는 방식을 변경할 수도 있습니다.react-server-dom-webpack 내부에 있음을 보여줄 수 있습니다.react-server-dom-webpack은 Webpack 번들 내부에서 실행되고 있다고 가정합니다. 우리의 webpack-runtime.js는 이 라이브러리가 기대하는 전역 변수(__webpack_require__, __webpack_chunk_load__)를 제공합니다.이것은 취약점을 흉내 내는 것이 아니라, 라이브러리가 실제로 작동하는 데 필요한 최소한의 런타임을 제공하는 것뿐입니다.
개발자가 child_process.exec와 같은 위험한 요소를 의도적으로 노출할 때만 작동하는 "유효하지 않은 PoC"에 대한 논의가 있었습니다.
이 PoC는 그런 종류가 아닙니다. 일반적이고 안전한 설정에서 작동합니다.
노출된 함수는 무해합니다
앱은 문자열만 반환하고 아무런 위험한 일도 하지 않는 간단한 updateProfile 함수를 노출합니다. 셸 명령은 없습니다.
익스플로잇은 해당 함수를 완전히 벗어납니다
취약점으로 인해 공격자는 안전한 export를 무시하고 전역 Function 생성자인 updateProfile.constructor로 바로 이동할 수 있습니다.
핵심 문제는 속성 접근입니다
React는 .constructor에 대한 접근을 허용하지 말았어야 합니다. 개발자는 Function 생성자를 노출하려는 의도가 없었지만, 안전하지 않은 역직렬화가 대신 그렇게 만든 것입니다.
유일한 실제 요구 사항은 모듈이 함수를 직접 내보낸다는 것(module.exports = fn)인데, 이는 CommonJS와 많은 번들러 설정에서 매우 일반적입니다.
exploit.js의 페이로드는 세 개의 청크로 React Flight 메시지를 구성합니다:
id: "user-profile-action#constructor" — "생성자를 달라"는 의미입니다.bound: 인수를 포함하는 청크 2를 가리킵니다.["console.log('nice try, diddy!')"]: 악성 코드 문자열입니다.React가 이를 역직렬화하면:
user-profile-action을 해석합니다..constructor 속성을 읽어 전역 Function을 얻습니다.new Function("console.log('nice try, diddy!')")그리고 그것이 RCE입니다!
패치된 버전으로 즉시 업그레이드하세요:
react-server-dom-webpack >= 19.0.1react-server-dom-parcel >= 19.0.1react-server-dom-turbopack >= 19.0.1패치에서는 상속된 속성에 대한 접근을 방지하기 위해 hasOwnProperty 검사를 도입하고 base64 파일 업로드를 제한합니다.
패치된 버전에 대해 이 PoC를 실행하면 서버가 충돌하거나 다음과 같은 오류가 발생합니다:
$ node --conditions react-server server.js
Listening on http://localhost:3000
/path/to/CVE-2025-55182/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js:2726
resolvedValue = resolvedValue.bind.apply(
^
TypeError: Cannot read properties of undefined (reading 'bind')
at /path/to/CVE-2025-55182/node_modules/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js:2726:43
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
Node.js v20.19.3
이를 통해 익스플로잇이 constructor 속성에 접근하지 못했고(Function 대신 undefined를 반환) 그 결과 후속 .bind 호출이 실패했음을 확인할 수 있습니다.
이 코드는 교육 및 테스트 목적으로만 제공됩니다. 소유하지 않았거나 테스트에 대한 명시적 허가를 받지 않은 시스템에 이 익스플로잇을 사용하지 마십시오.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE에 따라 배포됩니다.