
취약점 개요
| 항목 | 내용 |
| CVE ID | CVE-2026-42559 |
| 컴포넌트 | rmcp (Model Context Protocol 공식 Rust SDK) |
| 취약 위치 | crates/rmcp/src/transport/streamable_http_server/tower.rs |
| 취약 버전 | rmcp < 1.4.0 |
| 패치 버전 | rmcp >= 1.4.0 (커밋 8e22aa2, PR #764) |
| CVSS 3.1 | 8.8 (High) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| CWE | CWE-346 (Origin Validation Error), CWE-350 (Reliance on Reverse DNS Resolution) |
| 취약점 유형 | DNS Rebinding → 로컬 서비스 접근 |
rmcp는 Anthropic이 만든 Model Context Protocol(MCP)의 공식 Rust SDK로, LLM 클라이언트와 도구(tool)를 제공하는 MCP 서버 사이의 통신 계층을 구현한다. 이 SDK로 만든 MCP 서버는 대부분 개발자의 로컬 머신(127.0.0.1)에서 파일시스템·쉘·브라우저 제어 같은 강력한 툴을 노출한 채로 떠 있는데, 문제는 이 서버의 Streamable HTTP transport가 들어오는 요청의 Host 헤더를 전혀 검증하지 않았다는 점이다. 이 한 줄의 누락이 DNS Rebinding 공격과 결합되면서, 피해자가 악성 웹페이지를 열어보기만 해도 로컬 MCP 서버의 툴을 임의로 호출할 수 있는 통로가 열린다.
DNS Rebinding 공격 원리
브라우저의 동일 출처 정책(SOP)은 도메인 이름을 기준으로 출처를 판단하고, 요청을 실제로 어디로 보낼지는 DNS가 그 순간에 반환하는 IP에 맡긴다. DNS Rebinding은 이 틈을 노린다.
- 공격자가 자신이 소유한 도메인(attacker.example)의 DNS TTL을 매우 짧게(예: 1초) 설정해둔다.
- 피해자가 해당 도메인으로 접속하면, 처음엔 정상적인 공인 IP를 반환해 페이지를 정상적으로 로드시킨다.
- 페이지 내 JavaScript가 TTL 만료 후 같은 도메인으로 다시 요청을 보내면, 이번엔 DNS 응답을 127.0.0.1(또는 사설 대역)로 바꿔치기한다.
- 브라우저 입장에서는 여전히 "같은 출처(attacker.example)"로 보이기 때문에 SOP를 통과하며, 실제 TCP 연결은 피해자 로컬 머신으로 맺어진다.
- 이때 HTTP 요청의 Host 헤더는 여전히 attacker.example로 전송되지만, 수신 서버는 이 로컬 머신 — 즉 rmcp로 띄운 MCP 서버 — 이다.
여기서 서버가 Host 헤더 값을 확인해서 "내가 알고 있는 이름(localhost, 127.0.0.1 등)이 아니면 거부"하는 로직만 있었다면 이 공격은 5번 단계에서 막힌다. 그런데 패치 전 rmcp에는 그 로직 자체가 없었다.

패치 전: 무엇이 빠져 있었나
GitHub 보안 권고(GHSA-89vp-x53w-74fx)에 따르면, 1.4.0 이전 버전의 Streamable HTTP 서버는 요청이 들어오면 프로토콜 버전 검사(MCP-Protocol-Version), 세션 ID 검사, JSON-RPC 파싱으로 바로 진입했고 Host/Origin 헤더를 대조하는 단계 자체가 파이프라인에 존재하지 않았다. 개념적으로 표현하면 아래와 같다.

즉 서버는 요청이 "누구에게서 왔는가"가 아니라 "MCP 프로토콜 규격에 맞는가"만 검사했다. 요청을 보낸 소켓이 로컬(127.0.0.1)에서 왔다는 사실만으로 신뢰할 수 있는 클라이언트라고 암묵적으로 가정한 셈인데, DNS Rebinding은 정확히 이 가정 — "루프백으로 온 요청 = 신뢰 가능"과 "Host 헤더 값 = 실제 통신 상대" 사이의 괴리 — 을 깨뜨린다.
영향 범위는 보고서에 명시된 대로 상당히 넓다.
- MCP 서버가 노출한 모든 툴을 열거(tools/list)하고 임의로 호출(tools/call)
- 세션을 통해 접근 가능한 리소스·프롬프트·상태 조회
- 파일 쓰기, 쉘 명령 실행, 외부 API 호출 등 툴이 가진 부작용을 그대로 트리거
MCP 서버는 보통 사용자 권한으로 실행되며 파일시스템·쉘·브라우저 제어 같은 개발 도구를 노출하는 경우가 많기 때문에, 실질적인 영향은 피해자 PC에서의 임의 코드 실행까지 이어질 수 있다.
패치 이후: 실제 소스코드 분석
패치는 크게 세 가지로 구성된다. ① 기본값으로 루프백 호스트만 허용하는 allowlist 도입, ② 모든 요청이 거치는 validate_dns_rebinding_headers 검증 단계 삽입, ③ Origin 헤더에 대한 부가적(defense-in-depth) 검증. 실제 배포된 tower.rs 소스를 한 단계씩 뜯어보자.
1) 기본 allowlist: StreamableHttpServerConfig

핵심은 Default 구현이다. 별도 설정 없이 StreamableHttpServerConfig::default()를 쓰기만 해도 allowed_hosts가 localhost, 127.0.0.1, ::1 세 개로 채워진다. 즉 "안전하지 않은 기본값(insecure by default)"에서 "안전한 기본값(secure by default)"으로 전환한 것이 이번 패치의 설계 철학이다. 공인 서버로 배포하려는 개발자는 with_allowed_hosts(["mcp.example.com"])로 명시적으로 넓혀야 하고, 반대로 검증을 끄고 싶다면 disable_allowed_hosts()를 호출해야 한다 — 기본 동작이 아니라 의식적인 선택을 거치도록 강제한 것이다.

allowed_hosts가 빈 벡터가 되면 뒤에서 살펴볼 host_is_allowed가 "비어 있으면 전부 허용"으로 처리하므로, disable_allowed_hosts()는 문자 그대로 검증 자체를 무력화한다.
2) 요청 진입점에서의 강제 검사

패치의 가장 중요한 지점은 위치 선정이다. validate_dns_rebinding_headers가 handle() 최상단, 즉 메서드 분기(match method)와 세션 조회보다 먼저 실행된다. 검증에 실패하면 세션 상태를 건드리거나 JSON-RPC 바디를 파싱하기 전에 즉시 응답을 반환해버리므로, 공격 표면이 "요청이 서버 내부 로직에 도달하기 전" 단계로 확 줄어든다.
3) Host 헤더 파싱 — parse_host_header

여기서 눈여겨볼 부분은 HTTP/2 대응이다. HTTP/1.1은 Host: 헤더로 대상을 지정하지만, HTTP/2는 요청 라인의 :authority 의사 헤더로 이를 대체한다. 일부 미들웨어(예: axum::Router::nest)가 라우팅 과정에서 hyper가 합성해준 Host 헤더를 지워버릴 수 있기 때문에, 헤더가 없을 경우 request.uri()의 authority를 폴백으로 사용한다. 즉 "Host 헤더가 없으면 통과"가 아니라 "Host 헤더가 없으면 URI의 authority라도 반드시 확인한다"는 방어적 설계다.
4) 호스트 정규화 — 대소문자·IPv6 괄호 처리

DNS 이름은 대소문자를 구분하지 않고(Localhost와 localhost는 같은 호스트), IPv6 리터럴은 URI 상에서 [::1]처럼 대괄호로 감싸 표기된다. 이 두 가지를 정규화해두지 않으면 Host: LOCALHOST나 Host: [::1] 같은 변형으로 allowlist 비교를 우회당할 수 있다. normalize_host는 이 두 케이스를 한 번에 처리해서, 이후 비교 로직이 순수 문자열 == 비교만으로 안전하게 동작하도록 만든다.
5) allowlist 매칭 — host_is_allowed

설정값("example.com", "example.com:8080" 등)을 Authority로 파싱해서 호스트/포트를 분리한 뒤 비교한다. 포트를 지정하지 않은 allowlist 항목("localhost")은 어떤 포트로 들어오든 매치되고, 포트까지 지정한 항목("example.com:8080")은 정확히 그 포트만 허용한다. 이 유연성 덕분에 개발 중에는 localhost(포트 무관)로 넓게 열어두고, 운영 환경에서는 example.com:443처럼 좁혀 쓸 수 있다.
6) 최종 검증 함수 — validate_dns_rebinding_headers

이 함수 하나가 패치의 핵심이다. Host 헤더를 파싱해 allowlist에 없으면 즉시 403 Forbidden을 반환하고, tracing::warn!으로 DNS Rebinding 시도 가능성을 로그에 남긴다. 앞서 다이어그램의 5번 단계에서 Host: attacker.example로 도착한 요청은 이 지점에서 걸러진다 — attacker.example은 기본 allowlist(localhost, 127.0.0.1, ::1) 어디에도 속하지 않기 때문이다.
7) Origin 헤더 검증 — 심층 방어

권고문에 명시된 대로, 이번 CVE의 근본 원인은 어디까지나 Host 헤더 미검증이며 Origin 검증은 필수가 아니다 — 브라우저는 리바인딩된 대상에게 보내는 요청의 Host 헤더 자체를 위조할 수 없으므로, Host allowlist만으로 이번 공격은 차단된다. 그럼에도 Origin 검증을 추가한 이유는 심층 방어(defense-in-depth) 때문이다. 기본값은 allowed_origins가 비어 있으면 검증을 건너뛰도록(하위 호환성 유지) 설계되어 있고, enforce_origin_validation()을 호출하면 빈 리스트 상태에서도 Origin 헤더가 존재하는 모든 요청을 거부하는 엄격 모드로 전환할 수 있다.
대응 방안
- rmcp 1.4.0 이상으로 업그레이드한다. 기본 설정만으로도 루프백 이외의 Host를 가진 요청은 차단된다.
- 공인 도메인으로 MCP 서버를 배포한다면 StreamableHttpServerConfig::default().with_allowed_hosts(["mcp.example.com"])처럼 실제 호스트명을 명시적으로 등록해야 한다.
- 즉시 업그레이드가 어렵다면, Host 헤더가 예상한 값이 아닌 요청을 거부하도록 설정한 리버스 프록시(nginx, Caddy 등)를 앞단에 두고, MCP 서버를 0.0.0.0에 직접 바인딩하지 않는다.
- disable_allowed_hosts()는 이번 취약점이 패치되기 이전 상태로 되돌리는 옵션이므로, 업스트림에 별도의 Host 검증 프록시가 없다면 사용하지 않는다.
'CVE 취약점' 카테고리의 다른 글
| CVE-2026-8932: curl/libcurl mTLS 커넥션 재사용 취약점 분석 (0) | 2026.09.23 |
|---|---|
| CVE-2026-17633 분석 — Langflow OSS 인증된 원격 코드 실행(RCE) (0) | 2026.09.22 |
| CVE-2026-16723 : Fastjson 1.x @JSONType 신뢰 분기를 악용한 Pre-Auth RCE (0) | 2026.09.17 |
| CVE-2026-85706 분석 — GitLab Repository Commits API 인증 우회 임의 파일 읽기 (CVSS 10.0) (0) | 2026.09.15 |
| Feast Registry gRPC RCE — dill.loads() 하나로 뚫리는 인가 체크 (CVE-2026-56121) (0) | 2026.09.13 |