React RCE 사건이 남긴 교훈: 왜 지금 HMAC 서명과 키 로테이션, 제로 트러스트인가
RCE 취약점이 보여준 것: “데이터를 믿는 순간 끝난다”
최근 React Server Components/Next.js에서 발생한 RCE 취약점(CVE-2025-55182)은 단순히 “React가 뚫렸다”라는 뉴스로 끝낼 일이 아니다. 이 사건이 보여준 핵심 메시지는 훨씬 더 원칙적이다.
“클라이언트가 보낸 데이터를 한 번이라도 신뢰하면, 언젠가는 반드시 뚫린다.”
이번 취약점의 본질은, 서버가 클라이언트가 보낸 Flight 프로토콜 데이터(메타데이터)를 충분한 검증 없이 모듈 로딩·객체 접근에 바로 사용했다는 점이다.
- “이 값은 React가 알아서 안전하게 줄 거야.”
- “우리가 직접 쓰는 API가 아니니까 괜찮겠지.”
이런 식의 묵시적 신뢰가 누적된 끝에, 사전 인증 없이도 RCE가 가능한 지점까지 이어진 것이다.
이제 질문을 바꿔야 한다.
- “React가 왜 뚫렸냐?”가 아니라
- “우리는 지금 어떤 데이터를, 아무 서명·검증 없이 그대로 믿고 있나?”
그 질문의 답을 찾는 과정에서 자연스럽게 등장해야 할 개념이 바로 HMAC 서명과 제로 트러스트다.
왜 HMAC 서명이 중요한가: “이 데이터가 정말 우리 서버가 만든 것인가?”
현실의 시스템은 생각보다 “서버끼리의 신뢰”에 매우 의존한다.
- 프론트엔드 ↔ 백엔드
- 마이크로서비스 A ↔ B
- 백엔드 ↔ 백엔드(비동기 작업, 큐, Webhook, 내부 API 등)
이 사이에서 오가는 데이터는 종종 이렇게 가정된다.
“이 URL은 외부에 안 노출했으니까 내부 시스템만 호출할 것이다.”
“이 토큰 형식은 우리 서비스만 알고 있으니까 안전하다.”
하지만 공격자는 항상 이 “가정”부터 깨려고 한다.
여기서 HMAC(HMAC-SHA256 같은 메시지 인증 코드)는 한 가지 근본적인 질문에 답하기 위해 존재한다.
이 요청/메시지를, 정말로 “우리 쪽이 가진 비밀 키”를 아는 누군가가 만들었나?
조금 더 풀어 쓰면:
- HMAC 키를 알고 있는 쪽만 유효한 서명(signature)을 생성할 수 있고
-
서버는
payload + signature를 받아서 -
“서명이 맞는지”,
- “중간에 변조되지 않았는지”를 확인할 수 있다.
HMAC 서명이 없는 세계
HMAC 서명이 없는 REST/Webhook/내부 API는 어떻게 될까?
- 공격자가 URL 구조와 파라미터를 어느 정도 알아냈다고 가정하면,
- 임의 요청을 만들어 보내고, 서버가 그걸 “자연스러운 내부 요청”으로 착각하게 만들 수 있다.
예를 들어,
POST /internal/run-action
Content-Type: application/json
{
"action": "promoteUser",
"userId": 123
}
원래는 “백오피스에서만 호출하는 내부 엔드포인트”였다고 해도,
- 네트워크 경계가 한 번 뚫리거나,
- 내부 프록시 취약점이 있거나,
- CI/CD, 로그, 샘플 코드, 문서가 외부에 노출되면
공격자는 마치 내부 시스템인 척 이 엔드포인트를 두드릴 수 있다.
이때, HMAC 서명이 있었다면 상황은 완전히 달라진다.
- 요청 바디 전체에 HMAC 서명을 붙이고,
- 서버는 매 요청마다
signature를 검증한다면,
“URL을 알아도, 파라미터를 알아도, 키가 없으면 유효한 요청을 만들 수 없다”
라는 상태가 된다.
예시: HMAC 서명으로 내부 액션 보호하기
간단한 예를 보자. (TypeScript/Node.js 느낌의 의사 코드)
1. 서버가 공유 비밀 키를 가진 상태라고 가정
const HMAC_SECRET = process.env.HMAC_SECRET!; // .env나 Secret Manager에서 주입
2. 클라이언트(또는 내부 서비스)가 요청을 만들 때
import crypto from 'crypto';
function signPayload(payload: object): string {
const json = JSON.stringify(payload);
return crypto
.createHmac('sha256', HMAC_SECRET)
.update(json)
.digest('hex');
}
const payload = {
action: 'promoteUser',
userId: 123,
};
const signature = signPayload(payload);
// 전송
fetch('/internal/run-action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Signature': signature,
},
body: JSON.stringify(payload),
});
3. 서버가 검증할 때
function verifySignature(payload: any, signature: string): boolean {
const json = JSON.stringify(payload);
const expected = crypto
.createHmac('sha256', HMAC_SECRET)
.update(json)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex'),
);
}
app.post('/internal/run-action', (req, res) => {
const signature = req.headers['x-signature'];
if (!signature || !verifySignature(req.body, String(signature))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 여기까지 왔다는 것은:
// 1) payload가 중간에 변조되지 않았고
// 2) HMAC_SECRET을 아는 주체가 만든 요청이라는 의미
handleInternalAction(req.body);
});
이 구조가 바로 “이 데이터가 정말 우리 쪽이 보낸 것인지”를 증명하는 가장 기본적인 패턴이다.
관리 직원(운영자/개발자)이 여럿이라면: HMAC 키 로테이션은 “선택이 아니라 필수”
HMAC 키의 또 다른 현실적인 문제는 사람이다.
- 운영자, SRE, 백엔드 개발자, 외주 인력, 인턴…
- 모두가
.env에 접근할 수 있는 것은 아니어도, - GitOps, CI/CD, 문서, 슬랙, 노션 등 어딘가에서 키가 노출될 가능성은 항상 있다.
여기서 중요한 건:
“키가 유출될 수 있다”라는 가정을 전제로 움직이는 것이다.
즉, “언젠가는 유출된다”라고 보고 설계해야 한다.
그래서 HMAC 키 로테이션(rotation)이 필수가 된다.
왜 로테이션이 필요한가
- 사람이 바뀐다
- 퇴사, 팀 변경, 외주 종료 등
- 과거에 키를 볼 수 있었던 사람이 이제는 더 이상 볼 수 없어야 한다.
- 키 노출을 완전히 추적할 수 없다
- 과거 Slack DM, 로컬 노트, 캡처, 메모…
- “우리는 한 번도 키를 유출한 적이 없다”는 보장은 사실상 불가능하다.
- 사고 이후에도 복구 가능한 구조가 필요하다
- 만약 특정 시점 이전의 키가 유출되었다면,
- “그 이후부터는 새 키로만 서명된 요청만 받는다”라고 선언할 수 있어야 한다.
실무에서의 HMAC 키 로테이션 패턴
운영자/관리 직원이 여럿인 조직이라면 보통 이런 패턴을 권장한다.
- 키 버전 관리
HMAC_SECRET_V1,HMAC_SECRET_V2같이 버전을 두고- 요청 헤더나 페이로드에
kid(key id)를 함께 전송한다.
- 서버는 여러 키를 동시에 들고 있다가, 검증 시 버전별로 처리
- 예: V1, V2를 모두 검증 가능하게 한 뒤
- 일정 기간 후 V1 폐기, V2만 허용
- 로테이션 프로세스를 문서화
- “새 키 생성 → 배포 → 양쪽(생성자/검증자) 모두 새 키로 전환 → 이전 키 제거” 과정을
- check list + runbook으로 만들어 두고, 사람이 바뀌어도 그대로 따라 할 수 있게 한다.
- 권한 분리
- 키를 생성/관리하는 사람과
- 애플리케이션 코드를 수정하는 사람을 분리하는 것도 가능하면 고려
HMAC 키가 “한 번 정하면 끝인 값”이 되는 순간, 키 유출 = 시스템 전체 붕괴가 된다.
키 로테이션은 이 리스크를 시간에 따라 제한하는 기술적 장치다.
제로 트러스트: “신뢰는 구조가 아니라, 매 요청에서 만들어야 한다”
마지막으로, 이번 React RCE 사건을 통해 다시 상기해야 할 보안의 기본 원칙이 있다.
보안의 기본은 제로 트러스트(Zero Trust)다.
제로 트러스트의 핵심은 아주 단순하다.
- “내부니까 괜찮겠지”
- “프론트에서만 쓰는 값이니까 안전하겠지”
- “우리가 만든 프레임워크니까 알아서 잘 막혀 있겠지”
이런 문장을 기본적으로 금지하는 사고방식이다.
대신 이렇게 질문해야 한다.
- “이 입력이, 악의적일 수 있다는 가정 하에서 설계했는가?”
- “이 요청이 정말로 우리가 의도한 주체로부터 왔다는 증거가 있는가?”
- “이 데이터가 중간에 변조되지 않았다는 걸 어떻게 확인하는가?”
- “이 키가 유출되면, 어떤 범위까지 영향이 가고, 어떻게 회수·복구할 수 있는가?”
React RCE 사건도 같은 프레임에서 볼 수 있다.
- RSC Flight 데이터는 “React가 주는 거니까”라는 신뢰 아래 처리되었다.
- 그 결과, 클라이언트가 조작할 수 있는 값이 서버 모듈 로딩 경로에 직접 닿아 있었다.
제로 트러스트 관점에서 보면 이 구조는 처음부터 이렇게 보였어야 한다.
“클라이언트가 조작 가능한 직렬화된 데이터가 서버 코드 실행 경로에 영향을 준다 = 무조건 위험 시그널”
그리고 이런 곳에서 가장 먼저 떠올라야 할 방어책 중 하나가 바로:
- HMAC 서명(무결성·출처 확인)
- 키 로테이션(키 유출 가정 하에 피해 범위 제한)
이다.
정리: “신뢰하지 말고, 증명하게 만들어라”
이번 글에서 말하고 싶은 핵심 메시지는 딱 세 가지다.
- 데이터를 기본적으로 신뢰하지 말 것
- 특히, “프레임워크가 내부적으로 사용하는 데이터”라 해도
- 그게 네트워크를 건너왔다면 언제든 공격 표면이 될 수 있다.
- HMAC 서명은 “이 데이터가 정말 우리 쪽에서 온 것인지”를 확인하는 기본 도구
- REST API, Webhook, 내부 RPC, 비동기 메시지 등
- “서버 간 통신”이 있는 모든 지점에 HMAC를 고려하라.
- 관리 인력이 여럿인 환경이라면, 키 로테이션을 프로세스 레벨에서 필수로 가져가라.
- 보안의 기본은 제로 트러스트
- “내부니까 괜찮겠지”라는 말이 나오는 순간, 이미 위험 신호다.
- 신뢰는 구조 설계로 자동 주어지는 것이 아니라,
- 매 요청마다, 매 데이터마다, 검증을 통해 새로 만들어지는 것이다.
React RCE 사건은 프론트엔드와 서버의 경계가 흐려지는 시대에 우리가 어떤 태도를 가져야 하는지 다시 한 번 상기시켜 준다.
“신뢰하지 말고, 검증하라. 검증을 코드로, 프로세스로, 키 관리 정책으로 남겨라.”
그 첫걸음이 바로 HMAC 서명과 키 로테이션이며, 그 위에 서 있는 철학이 제로 트러스트다.

댓글이 없습니다.