MCP(Model Context Protocol)가 인가(authorization) 공식 사양을 발표했다. 하지만 MCP 서버를 만드는 대다수 개발자들은 인증을 아예 생략하거나, 기껏해야 단순한 JWT 미들웨어 하나 얹어두고 끝내곤 한다. 그러나 공식 사양은 생각보다 훨씬 정교하며, 공격 표면(attack surface) 역시 많은 이들의 예상보다 훨씬 넓다.
기본 개념 (OAuth를 안다면 건너뛰어도 좋다)
MCP는 원격 서버 통신에 OAuth 2.1을 채택했다. 흐름은 표준적이다: 클라이언트가 서버를 호출하면, 서버는 보호 자원 메타데이터(Protected Resource Metadata, PRM) 문서를 가리키는 WWW-Authenticate 헤더와 함께 401 Unauthorized 응답을 반환한다. 이 문서는 클라이언트에게 어떤 인가 서버(Authorization Server)와 통신해야 하는지 알려준다. 클라이언트는 인가 서버의 엔드포인트를 찾아 동적 또는 사전 등록을 마치고, 사용자가 브라우저에서 동의하면 토큰을 발급받아 이를 사용한다.
로컬 STDIO 서버의 경우 이 복잡한 과정이 필요 없다. 환경 변수나 임베디드 자격증명 등 상황에 맞는 방식을 쓰면 된다. OAuth는 서버가 원격으로 호스팅되는 HTTP 기반 트랜스포트를 위한 것이다.
흥미로운 지점은 정상 흐름(happy path)이 아니라, 문제가 터지는 예외 상황들이다.
대리인 혼동(Confused Deputy) 문제 — 가장 치명적인 위협
사양서에서 가장 많은 지면을 할애해 설명하는 공격이며, 그럴 만한 이유가 있다.
MCP 프록시 서버를 상상해 보자. MCP 클라이언트와 서드파티 API(GitHub이나 Slack API를 감싼 MCP 서버) 사이에 위치하는 프록시다. 이 프록시는 서드파티 인가 서버에 대해 정적 클라이언트 ID를 갖고 있고, 개별 MCP 클라이언트들은 프록시에 동적으로 등록하며 각자의 클라이언트 ID를 부여받는다.
공격 시나리오는 다음과 같다:
- 피해자 사용자가 정상적인 방법으로 프록시를 통해 서드파티 API에 접근 인증을 완료한다. 서드파티 인가 서버는 사용자의 브라우저에 동의 쿠키를 설정한다.
- 공격자가 프록시에 악의적인 MCP 클라이언트를 동적 등록하면서
redirect_uri를attacker.com으로 지정한다. - 공격자는 피해자에게 프록시를 통한 인가 요청을 유발하는 조작된 링크를 보낸다.
- 피해자의 브라우저에는 1단계에서 저장된 동의 쿠키가 남아 있다. 서드파티 인가 서버는 이 쿠키를 보고 동의 화면을 생략한다.
- 인가 코드(authorization code)가 공격자의 서버(
attacker.com)로 리다이렉트된다. - 공격자는 이 코드를 이용해 액세스 토큰을 탈취한다. 공격 성공이다.
이 문제의 해결책은 요청이 서드파티 인가 서버로 전달되기 전에, **MCP 서버 수준에서 클라이언트별 개별 동의(per-client consent)**를 강제하는 것이다. MCP 프록시는 요청한 MCP 클라이언트의 이름과 요구 스코프, 리다이렉트 URI를 명시하고 사용자의 명시적 승인을 요구하는 자체 동의 페이지를 운영해야 한다. 이 동의 내역은 서버 측에 특정 client_id와 매핑되어 안전하게 저장되어야 한다.
또한 사양에서는 OAuth state 매개변수를 1회용이자 짧은 수명으로 제한할 것을 규정하고 있으며, 가장 결정적으로 사용자가 MCP 레벨의 동의 화면을 승인하기 전까지는 절대로 상태 쿠키(state cookie)를 브라우저에 구워서는 안 된다고 못 박고 있다. 승인 전에 쿠키를 먼저 심는다면 이 모든 방어책이 무력화되기 때문이다.
토큰 패스스루(Token Passthrough)의 명시적 금지
일부 개발자들은 MCP 서버가 클라이언트로부터 토큰을 받아 그대로 다운스트림 백엔드 API로 넘겨주는(passthrough) 중계자라고 착각한다. 사양은 단호하게 경고한다: 절대 이렇게 만들어서는 안 된다.
MCP 서버가 다른 서비스를 위해 발급된 토큰을 받아 그대로 전달하면, 프로토콜 수준에서 대리인 혼동 문제가 발생한다. 다운스트림 API는 당신의 서버가 유효성을 검증했다고 믿고 토큰을 신뢰하게 된다. 게다가 해당 토큰은 애초에 당신의 MCP 서버를 위해 발행된 것이 아니므로, 서버 자체의 레이트 리밋, 요청 검증, 감사 추적(audit trail)이 전부 무의미해진다.
항상 토큰의 aud(audience) 클레임이 자신의 MCP 서버 리소스 URL과 일치하는지 철저히 검증해야 한다. 자신을 위해 발급된 토큰이 아니라면 가차 없이 거부해야 한다.
OAuth 디스커버리 단계의 SSRF 취약점
간과하기 쉬운 매우 섬세한 공격 벡터다.
OAuth 디스커버리 과정에서 MCP 클라이언트는 여러 출처의 URL을 페치한다. WWW-Authenticate 헤더의 resource_metadata URL, PRM 문서에 기재된 authorization_servers URL, 인가 서버 메타데이터에 포함된 각종 엔드포인트 URL들이다.
악의적인 MCP 서버는 이 메타데이터 필드들에 내부 사설망 리소스를 가리키는 URL을 심어둘 수 있다:
http://169.254.169.254/latest/meta-data/— 클라우드 인스턴스 자격증명http://192.168.1.1/admin— 사내 내부망 서비스http://localhost:6379/— 로컬 Redis 인스턴스
클라이언트가 디스커버리 과정에서 이 URL들을 맹목적으로 호출하면, 순식간에 내부망을 공격하는 SSRF(서버 측 요청 위조) 프록시로 전락하게 된다.
방어책은 명확하다: HTTPS를 강제하고(개발 환경의 루프백 제외), 사설 IP 대역(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16)을 원천 차단하며, 리다이렉트 대상에도 동일한 제한을 걸고, 서버 사이드 배포 시에는 아웃바운드 이그레스 프록시를 적용해야 한다. 검사 시점과 사용 시점 사이를 파고드는 DNS 리바인딩(rebinding) 공격도 고려하여 호스트 해석을 고정해야 한다.
OAuth URL 인젝션
인가 플로우 도중 MCP 서버는 클라이언트가 브라우저에서 열 수 있도록 인가 URL을 전달한다. 악의적인 서버는 https:// 대신 javascript: 스키마를 포함한 URL을 내려보낼 수 있다. 클라이언트가 검증 없이 이를 window.open() 등에 전달하면 XSS가 발생한다. 만약 클라이언트가 셸 명령어를 통해 브라우저를 띄우도록 작성되어 있다면 명령어 인젝션(Command Injection)으로 이어진다.
특히 stdio를 통해 자식 프로세스로 MCP 서버를 구동하는 프록시 아키텍처와 결합될 경우, XSS는 호스트 시스템에서의 임의 코드 실행(RCE)으로 격상된다. 공격자가 클라이언트 환경에서 프록시 인증 토큰을 추출한 뒤 프록시를 조작하여 원하는 명령어를 실행할 수 있기 때문이다.
해결책: 오직 http://와 https:// 스키마만 허용할 것. URL을 열기 위해 셸 명령어를 절대 호출하지 말 것. 심층 방어를 위해 엄격한 CSP 헤더를 적용할 것.
스코프 최소화 — 번거롭지만 중요한 원칙
대부분의 MCP 서버는 자신이 지원하는 모든 스코프를 scopes_supported에 나열하고, 클라이언트는 이를 한 번에 전부 요청한다. 사용자는 모든 권한이 적힌 동의 화면을 보고 무심코 ’승인’을 누르며, 그 결과 최대 권한을 가진 토큰이 발급된다.
만약 이 토큰이 로그 노출, 메모리 스크래핑, 로컬 탈취 등으로 유출되면 공격자는 모든 시스템에 접근할 수 있게 된다. 이를 무효화하면 사용자의 모든 워크플로우가 중단된다.
더 나은 방법은 **점진적 권한 상승(progressive elevation)**이다. 처음에는 최소한의 스코프(읽기 전용 디스커버리를 위한 mcp:tools-basic)로 시작한다. 클라이언트가 더 높은 권한이 필요한 도구를 호출할 때, 서버가 해당 스코프를 명시한 WWW-Authenticate 챌린지를 반환하여 그 권한만을 추가로 재인가받도록 유도한다. 이렇게 하면 토큰 유출 시의 폭발 반경(blast radius)을 최소화할 수 있다.
처음부터 모든 권한을 한꺼번에 얻는 것보다 구현은 번거롭지만, 토큰 침해 사고가 단 하나의 작업에 그칠지 전체 시스템 붕괴로 이어질지를 결정하는 핵심 차이다.
실무에서 우선적으로 챙겨야 할 항목들
오늘 당장 MCP 서버를 구축한다면 다음 우선순위를 지켜야 한다:
1. 오디언스(Audience, aud) 검증: 토큰의 aud 클레임이 본인 서버와 정확히 일치하는지 확인하라. 가장 중요한 단 하나의 체크포인트다. 이것이 없으면 동일한 인가 서버가 발급한 아무 토큰이나 다 통과된다.
2. 프록시 서버의 클라이언트별 동의 구현: 서드파티 API를 래핑하는 프록시라면 자체 동의 화면을 구현하라. 서드파티의 동의 쿠키에 의존해선 안 된다.
3. 토큰 수명 단축: 리프레시 토큰을 활용하고 액세스 토큰 수명을 짧게 유지하라. 1시간짜리 토큰 탈취도 위험하지만, 30일짜리 토큰 탈취는 재앙이다.
4. OAuth 디스커버리 SSRF 방어: 사설 IP 차단, HTTPS 강제, 리다이렉트 검증을 철저히 하라.
5. 토큰 로깅 절대 금지: 너무나 당연해 보이지만 현업에서 매우 자주 어겨진다. Authorization 헤더, 쿼리 스트링, 구조화된 로그에서 토큰 값을 반드시 마스킹 처리하라.
구현 생태계 현황
사양 문서는 TypeScript, Python, C# 참조 구현체를 함께 제공한다. TypeScript와 Python SDK는 Keycloak 대상의 토큰 인트로스펙션(token introspection)을 지원하며, C# SDK는 Keycloak 권한 URL과 연동되는 ASP.NET Core의 내장 JWT 검증을 활용한다.
세 언어 구현체 모두 오디언스 검증과 PRM 디스커버리 플로우를 충실히 구현하고 있다. 특히 Python SDK의 MCPServer 클래스는 PRM 문서 발행, 적절한 헤더를 포함한 401 반환, 커스텀 검증기로의 토큰 전달 등 대부분의 보일러플레이트를 깔끔하게 자동 처리해 준다.
처음 시작한다면 Python SDK의 MCPServer와 커스텀 TokenVerifier를 조합하는 것이 표준 인가 플로우를 가장 빠르고 안전하게 구축하는 지름길이다.
맺으며
MCP의 인가 사양은 많은 개발자들의 생각보다 훨씬 깊은 보안적 고민을 담고 있다. 대리인 혼동 방어, SSRF 차단, 스코프 최소화 지침 등은 탁상공론이 아니라 실제 현장에서 벌어지는 공격 패턴들을 면밀히 분석한 결과다.
사용자 데이터나 사내 시스템을 다루는 MCP 서버를 운영하고 있다면 반드시 인가를 구현해야 한다. 서드파티 API 프록시를 만든다면 클라이언트별 동의 화면을 갖춰야 한다. 그리고 어떤 경우에도 aud 클레임 검증을 빠뜨려서는 안 된다.
명확한 사양이 있고, 공식 참조 구현체가 제공되며, 공격 시나리오도 모두 문서화되어 있다. 보안이 결여된 무방비 상태의 MCP 서버를 프로덕션에 올릴 핑계는 어디에도 없다.
참고자료: MCP Authorization Documentation, MCP Security Best Practices