diff --git a/src/fastmcp/server/auth/oauth_proxy/ui.py b/src/fastmcp/server/auth/oauth_proxy/ui.py index a8a12efe3..4ae6b5fb5 100644 --- a/src/fastmcp/server/auth/oauth_proxy/ui.py +++ b/src/fastmcp/server/auth/oauth_proxy/ui.py @@ -84,7 +84,7 @@ def create_consent_html( detail_rows = [ ("Application Name", html_module.escape(client_name or client_id)), ("Application Website", html_module.escape(client_website_url or "N/A")), - ("Application ID", client_id), + ("Application ID", html_module.escape(client_id)), ("Redirect URI", redirect_uri_escaped), ( "Requested Scopes", diff --git a/tests/server/auth/oauth_proxy/test_ui.py b/tests/server/auth/oauth_proxy/test_ui.py index 4795ec2ff..6f174aec2 100644 --- a/tests/server/auth/oauth_proxy/test_ui.py +++ b/tests/server/auth/oauth_proxy/test_ui.py @@ -7,7 +7,7 @@ from starlette.requests import Request from starlette.responses import HTMLResponse from fastmcp.server.auth.oauth_proxy import OAuthProxy -from fastmcp.server.auth.oauth_proxy.ui import create_error_html +from fastmcp.server.auth.oauth_proxy.ui import create_consent_html, create_error_html from fastmcp.server.auth.providers.jwt import JWTVerifier @@ -99,3 +99,21 @@ class TestErrorPageRendering: assert b"invalid_scope" in response.body assert b"doesn't exist" in response.body # HTML-escaped apostrophe assert b"OAuth Error" in response.body + + +class TestConsentPageRendering: + """Test consent page rendering and escaping.""" + + def test_create_consent_html_escapes_client_id_in_details(self): + """Test that Application ID is escaped in advanced details.""" + + html = create_consent_html( + client_id='evil', + redirect_uri="https://example.com/callback", + scopes=["read"], + txn_id="txn", + csrf_token="csrf", + ) + + assert 'evil' not in html + assert "evil<img src=x onerror=alert("xss")>" in html