Skip to content

Commit 340fdaa

Browse files
committed
fix: validate registered redirect uris
1 parent f475344 commit 340fdaa

2 files changed

Lines changed: 68 additions & 1 deletion

File tree

src/mcp/server/auth/handlers/register.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Any
55
from uuid import uuid4
66

7-
from pydantic import BaseModel, ValidationError
7+
from pydantic import AnyUrl, BaseModel, ValidationError
88
from starlette.requests import Request
99
from starlette.responses import Response
1010

@@ -18,12 +18,22 @@
1818
# provider from what we use in the HTTP handler
1919
RegistrationRequest = OAuthClientMetadata
2020

21+
_LOOPBACK_HOSTS = {"localhost", "127.0.0.1", "[::1]"}
22+
2123

2224
class RegistrationErrorResponse(BaseModel):
2325
error: RegistrationErrorCode
2426
error_description: str | None
2527

2628

29+
def _validate_redirect_uri(url: AnyUrl) -> None:
30+
if url.scheme != "https" and not (url.scheme == "http" and url.host in _LOOPBACK_HOSTS):
31+
raise ValueError("redirect_uris must use HTTPS unless they are HTTP loopback URLs")
32+
33+
if url.fragment is not None:
34+
raise ValueError("redirect_uris must not include a fragment")
35+
36+
2737
@dataclass
2838
class RegistrationHandler:
2939
provider: OAuthAuthorizationServerProvider[Any, Any, Any]
@@ -45,6 +55,19 @@ async def handle(self, request: Request) -> Response:
4555
status_code=400,
4656
)
4757

58+
if client_metadata.redirect_uris is not None:
59+
try:
60+
for redirect_uri in client_metadata.redirect_uris:
61+
_validate_redirect_uri(redirect_uri)
62+
except ValueError as error:
63+
return PydanticJSONResponse(
64+
content=RegistrationErrorResponse(
65+
error="invalid_redirect_uri",
66+
error_description=str(error),
67+
),
68+
status_code=400,
69+
)
70+
4871
client_id = str(uuid4())
4972

5073
# If auth method is None, default to client_secret_post

tests/server/mcpserver/auth/test_auth_integration.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,50 @@ async def test_client_registration(self, test_client: httpx.AsyncClient, mock_oa
656656
# client_info["client_id"]
657657
# ) is not None
658658

659+
@pytest.mark.anyio
660+
async def test_client_registration_allows_loopback_redirect_uri(self, test_client: httpx.AsyncClient):
661+
"""Test client registration with an HTTP loopback redirect URI."""
662+
client_metadata = {
663+
"redirect_uris": ["http://localhost:3030/callback"],
664+
"client_name": "Loopback Client",
665+
}
666+
667+
response = await test_client.post(
668+
"/register",
669+
json=client_metadata,
670+
)
671+
assert response.status_code == 201, response.content
672+
assert response.json()["redirect_uris"] == ["http://localhost:3030/callback"]
673+
674+
@pytest.mark.anyio
675+
@pytest.mark.parametrize(
676+
"redirect_uri",
677+
[
678+
"http://client.example.com/callback",
679+
"javascript:alert(1)",
680+
"data:text/html,<script>alert(1)</script>",
681+
"file:///tmp/callback",
682+
"https://client.example.com/callback#fragment",
683+
"https://client.example.com/callback#",
684+
],
685+
)
686+
async def test_client_registration_rejects_unsafe_redirect_uris(
687+
self, test_client: httpx.AsyncClient, redirect_uri: str
688+
):
689+
"""Test client registration rejects unsafe redirect URI schemes and fragments."""
690+
client_metadata = {
691+
"redirect_uris": [redirect_uri],
692+
"client_name": "Test Client",
693+
}
694+
695+
response = await test_client.post(
696+
"/register",
697+
json=client_metadata,
698+
)
699+
assert response.status_code == 400
700+
error_data = response.json()
701+
assert error_data["error"] == "invalid_redirect_uri"
702+
659703
@pytest.mark.anyio
660704
async def test_client_registration_missing_required_fields(self, test_client: httpx.AsyncClient):
661705
"""Test client registration with missing required fields."""

0 commit comments

Comments
 (0)