From 8e5631ea7bc59fb7bb5408e60c93b95cd884ad1a Mon Sep 17 00:00:00 2001 From: kcncb <128760047+kcncb@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:26:16 +0800 Subject: [PATCH] fix(client): reject userinfo, dot segments and fragments in CIMD client_id URLs --- src/mcp/client/auth/utils.py | 17 +++++++++++++---- tests/client/test_auth.py | 13 +++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/mcp/client/auth/utils.py b/src/mcp/client/auth/utils.py index 0a2ba80aec..44629b3ddf 100644 --- a/src/mcp/client/auth/utils.py +++ b/src/mcp/client/auth/utils.py @@ -1,6 +1,6 @@ import re from typing import Any, cast -from urllib.parse import urljoin, urlparse +from urllib.parse import unquote, urljoin, urlparse from httpx2 import Request, Response from mcp_types import LATEST_PROTOCOL_VERSION @@ -320,19 +320,28 @@ async def handle_registration_response(response: Response) -> OAuthClientInforma def is_valid_client_metadata_url(url: str | None) -> bool: """Validate that a URL is suitable for use as a client_id (CIMD). - The URL must be HTTPS with a non-root pathname. + Draft-ietf-oauth-client-id-metadata-document section 3 requires the URL to be HTTPS, to have a + non-root pathname, and to contain no userinfo, no `.`/`..` path segments and no fragment. Args: url: The URL to validate Returns: - True if the URL is a valid HTTPS URL with a non-root pathname + True if the URL satisfies the client metadata document URL rules """ if not url: return False try: parsed = urlparse(url) - return parsed.scheme == "https" and parsed.path not in ("", "/") + if parsed.scheme != "https" or parsed.path in ("", "/"): + return False + if parsed.username or parsed.password: + return False + if parsed.fragment: + return False + # Compare dot segments after percent-decoding: RFC 3986 treats %2E as a dot for the purposes + # of path segment normalisation, so "%2E%2E" is a ".." segment. + return not any(unquote(segment) in (".", "..") for segment in parsed.path.split("/")) except Exception: return False diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 18a1566705..3f7144b581 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -2363,6 +2363,19 @@ class TestCIMD: # Invalid URLs - root path ("https://example.com", False), ("https://example.com/", False), + # Invalid URLs - userinfo + ("https://user@example.com/client", False), + ("https://user:secret@example.com/client", False), + # Invalid URLs - `.` and `..` path segments + ("https://example.com/./client", False), + ("https://example.com/path/../client", False), + ("https://example.com/%2E%2E/client", False), + ("https://example.com/path/%2e/client", False), + # Invalid URLs - fragment + ("https://example.com/client#section", False), + # Valid URLs - dot-free segments, query strings and ports are allowed + ("https://example.com/path.with.dots/client", True), + ("https://example.com/client?version=2", True), # Invalid URLs - None or empty (None, False), ("", False),