Skip to content

Commit 8e5631e

Browse files
committed
fix(client): reject userinfo, dot segments and fragments in CIMD client_id URLs
1 parent a525a3d commit 8e5631e

2 files changed

Lines changed: 26 additions & 4 deletions

File tree

‎src/mcp/client/auth/utils.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import re
22
from typing import Any, cast
3-
from urllib.parse import urljoin, urlparse
3+
from urllib.parse import unquote, urljoin, urlparse
44

55
from httpx2 import Request, Response
66
from mcp_types import LATEST_PROTOCOL_VERSION
@@ -320,19 +320,28 @@ async def handle_registration_response(response: Response) -> OAuthClientInforma
320320
def is_valid_client_metadata_url(url: str | None) -> bool:
321321
"""Validate that a URL is suitable for use as a client_id (CIMD).
322322
323-
The URL must be HTTPS with a non-root pathname.
323+
Draft-ietf-oauth-client-id-metadata-document section 3 requires the URL to be HTTPS, to have a
324+
non-root pathname, and to contain no userinfo, no `.`/`..` path segments and no fragment.
324325
325326
Args:
326327
url: The URL to validate
327328
328329
Returns:
329-
True if the URL is a valid HTTPS URL with a non-root pathname
330+
True if the URL satisfies the client metadata document URL rules
330331
"""
331332
if not url:
332333
return False
333334
try:
334335
parsed = urlparse(url)
335-
return parsed.scheme == "https" and parsed.path not in ("", "/")
336+
if parsed.scheme != "https" or parsed.path in ("", "/"):
337+
return False
338+
if parsed.username or parsed.password:
339+
return False
340+
if parsed.fragment:
341+
return False
342+
# Compare dot segments after percent-decoding: RFC 3986 treats %2E as a dot for the purposes
343+
# of path segment normalisation, so "%2E%2E" is a ".." segment.
344+
return not any(unquote(segment) in (".", "..") for segment in parsed.path.split("/"))
336345
except Exception:
337346
return False
338347

‎tests/client/test_auth.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2363,6 +2363,19 @@ class TestCIMD:
23632363
# Invalid URLs - root path
23642364
("https://example.com", False),
23652365
("https://example.com/", False),
2366+
# Invalid URLs - userinfo
2367+
("https://user@example.com/client", False),
2368+
("https://user:secret@example.com/client", False),
2369+
# Invalid URLs - `.` and `..` path segments
2370+
("https://example.com/./client", False),
2371+
("https://example.com/path/../client", False),
2372+
("https://example.com/%2E%2E/client", False),
2373+
("https://example.com/path/%2e/client", False),
2374+
# Invalid URLs - fragment
2375+
("https://example.com/client#section", False),
2376+
# Valid URLs - dot-free segments, query strings and ports are allowed
2377+
("https://example.com/path.with.dots/client", True),
2378+
("https://example.com/client?version=2", True),
23662379
# Invalid URLs - None or empty
23672380
(None, False),
23682381
("", False),

0 commit comments

Comments
 (0)