MCP OAuth가 사용자별 리다이렉트 URI 신뢰로 피싱 구멍을 닫는다
MCP 서버에 OAuth 로그인을 붙이면 두 갈래 막다른 길이 나온다. 운영자가 허용한 콜백 주소만 받으면 주말에 직접 만든 에이전트는 등록 첫 단계에서 거부당한다. 반대로 아무 주소나 받아주면 그 서버는 피싱 경유지가 된다. Stream Vessel은 그 사이에 사용자별 신뢰 목록이라는 세 번째 자리를 만들었다. 서버 전체 허용 목록과 로컬 클라이언트용 루프백은 그대로 두고, 로그인 화면에서 사용자가 자기 계정에 한해 몇 개의 콜백 주소를 직접 신뢰하도록 한 것이다.
문제의 크기는 실측으로 확인됐다. 2026년 5월 OAuth를 켠 MCP 서버 119개를 조사한 연구에서 114개, 95.8%가 익명 요청자가 보낸 임의의 redirect_uri를 그대로 받아들였다. 공격은 네 단계로 끝난다. 공격자가 리다이렉트 주소를 자기 도메인으로 둔 'Claude'라는 이름의 클라이언트를 등록하고, 로그인한 사용자에게 저장소 재연결을 권하는 승인 링크를 보낸다. 사용자는 진짜 도메인의 진짜 동의 화면에서 승인을 누르고, 일회용 코드는 공격자 주소로 떨어진다. 코드를 토큰으로 바꾸면 공격자가 곧 그 사용자가 된다. PKCE는 이 흐름을 막지 못한다. 흐름을 시작한 쪽이 공격자이기 때문이다. 클라이언트 이름은 자기 신고일 뿐이고, 동의 화면에 찍히는 리다이렉트 호스트도 claude-connect.app처럼 한눈에 걸러지지 않는다.
실제 피해 사례도 있다. 2026년 1월 Obsidian Security는 Square의 MCP 서버에서 같은 사슬을 확인했다. 자기 등록이 아무 리다이렉트 주소나 받아들였고, 클릭 한 번으로 가맹점 프로필과 거래 내역, 은행 정보가 노출됐다. Square의 수정은 신규 등록 클라이언트에만 적용됐다. 기업 SSO 앞단에 놓인 MCP 프록시에서도 같은 사슬이 발견됐는데, 클릭 한 번에 24시간짜리 액세스 토큰과 14일짜리 리프레시 토큰이 넘어갔다. 조용한 부작용도 따라붙는다. 리다이렉트 주소를 받아들인 뒤 검사에 실패하면 에러가 그 주소로 전송되기 때문에, 아무 주소나 받는 서버는 자기 도메인 이름을 걸고 아무 곳으로나 보내주는 오픈 리다이렉터가 된다. RFC 9700이 정확히 이 상황을 다룬다.
MCP 스펙은 위험을 명시하되 정책은 각 서버에 맡긴다. 인가 서버가 리다이렉트 URI를 정확히 검증해야 하고(MUST), 신뢰하는 URI로만 자동 리다이렉트해야 하며(SHOULD), 대신 사용자에게 알리고 결정하게 할 수 있다(MAY)는 식이다. '신뢰'의 정의는 비어 있다. 등록 방식도 움직이고 있다. 2026-07-28 클라이언트 등록 문서는 사람이 개입하지 않는 API 기반 동적 클라이언트 등록(DCR)을 폐기하고, 클라이언트 ID를 리다이렉트 URI 목록을 담은 HTTPS 문서 주소로 삼는 CIMD로 방향을 튼다. 다만 CIMD가 알려주는 것은 누가 그 클라이언트를 게시했는지이지, 이 사용자가 그 클라이언트에 연결하려 했는지는 아니다. IETF OAuth 워킹그룹에서도 2025년 12월 스레드에서 인증 없는 등록이 MCP 인가 서버를 오픈 리다이렉터로 만든다는 지적이 나왔고, RFC 9700 §4.11.2가 바로 그 출발점을 다룬다.
현장은 양극단에 몰려 있다. pgEdge의 Postgres MCP 서버는 자기 등록을 열어두되 등록된 URI가 목록(기본값은 Claude 콜백과 두 개의 루프백 URI)에 있어야 한다. 반대편은 화이트리스트에 이름을 올린 클라이언트만 통과시키는 서버들로, 앞의 연구에서 119개 중 114개가 여기 해당했다. 폐쇄형의 비용은 실제 사용자가 치른다. 2026년 6월 Claude Code 이슈에는 정확 일치 리다이렉트 허용 목록을 쓰는 조직이 Claude Code로 로그인할 수 없다는 보고가 올라왔고, 그 클라이언트의 http://localhost:<포트>/callback 호스트와 경로가 고정이라는 설명이 붙었다. 이 이슈는 2026년 7월 처리하지 않음으로 닫혔다. Atlassian 개발자 커뮤니티에도 화이트리스트 도메인 밖 리다이렉트 URI가 거부된다는 오류가 사용자들에게 보고됐고, GitHub의 MCP 서버 정책은 호스트 애플리케이션마다 별도 OAuth 앱 등록을 요구한다. 그렇지 않으면 개인 액세스 토큰으로 내려가야 한다.
Stream Vessel의 선택은 검사 시점을 옮기는 것이다. 등록은 로그인보다 먼저 일어나므로 그 시점에는 누구의 목록을 볼지 알 수 없다. 그래서 등록 단계에서는 https URI를 일단 받아두고, 판단을 사용자가 확인되는 authorize 화면까지 미룬다. 코드든 에러든, 모든 계정 또는 해당 사용자가 신뢰한 주소가 아니면 절대 리다이렉트하지 않는다는 규칙이 모든 분기 밑에 깔린다. 로그인 전에는 어떤 사용자 목록도 적용되지 않으므로 PKCE 챌린지 오류나 알 수 없는 스코프 같은 OAuth 라이브러리 자체 검증 실패도 로그인 이후로 미뤄지고, 거부는 자기 호스트의 페이지로 끝난다. 거부 버튼도 같은 규칙을 탄다. 2026년 4월 n8n의 CVE-2026-42230은 동의를 거부한 사용자를 공격자가 등록한 주소로 보냈다.
MCP 클라이언트를 만드는 쪽이 얻는 것은 단순하다. 운영자에게 설정 파일 수정을 요청하지 않고도 자기 콜백을 쓸 수 있다. 서버를 운영한다면 등록 엔드포인트에서 URI를 검사하는 것으로 끝내지 말아야 한다. 등록 시점 검사는 사용자를 모르는 상태의 추측이고, 검사를 통과한 주소로 에러를 흘려보내는 순간 오픈 리다이렉터가 열린다. authorize 단계에서 사용자 신원과 함께 판단하고, 실패 응답조차 검증된 주소로만 내보내는 구조가 필요하다.
한계도 분명하다. 이 설계는 사용자가 자기 계정에 신뢰할 주소를 직접 등록하는 절차를 전제로 하고, Stream Vessel은 그 개수를 10개로 제한한다. 무엇을 신뢰할지 판단하는 부담은 사용자에게 남고, CIMD로 등록 방식이 바뀌어도 게시자 정보가 사용자의 연결 의도를 대신 증명해주지는 않는다.