Moderate severityNVD Advisory· Published Oct 28, 2025· Updated Oct 29, 2025
FastMCP vulnerable to reflected XSS in client's callback page
CVE-2025-62800
Description
FastMCP is the standard framework for building MCP applications. Versions prior to 2.13.0 have a reflected cross-site scripting vulnerability in the OAuth client callback page (oauth_callback.py) where unescaped user-controlled values are inserted into the generated HTML, allowing arbitrary JavaScript execution in the callback server origin. The issue is fixed in version 2.13.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
fastmcpPyPI | < 2.13.0 | 2.13.0 |
Affected products
1Patches
12a20f54617a3Escape all HTML to prevent XSS attack (#2090)
3 files changed · +167 −5
src/fastmcp/client/oauth_callback.py+1 −3 modified@@ -46,9 +46,7 @@ def create_callback_html( # Add detail info box for both success and error cases detail_info = "" if is_success and server_url: - detail_info = create_info_box( - f"Connected to: <strong>{server_url}</strong>", centered=True - ) + detail_info = create_info_box(f"Connected to: {server_url}", centered=True) elif not is_success: detail_info = create_info_box(message, is_error=True, centered=True)
src/fastmcp/utilities/ui.py+7 −2 modified@@ -7,6 +7,8 @@ from __future__ import annotations +import html + from starlette.responses import HTMLResponse # FastMCP branding @@ -339,6 +341,7 @@ def create_page( Returns: Complete HTML page as string """ + title = html.escape(title) return f""" <!DOCTYPE html> <html lang="en"> @@ -375,6 +378,7 @@ def create_status_message(message: str, is_success: bool = True) -> str: Returns: HTML for status message """ + message = html.escape(message) icon = "✓" if is_success else "✕" icon_class = "success" if is_success else "error" @@ -400,6 +404,7 @@ def create_info_box( Returns: HTML for info box """ + content = html.escape(content) classes = ["info-box"] if is_error: classes.append("error") @@ -422,8 +427,8 @@ def create_detail_box(rows: list[tuple[str, str]]) -> str: rows_html = "\n".join( f""" <div class="detail-row"> - <div class="detail-label">{label}:</div> - <div class="detail-value">{value}</div> + <div class="detail-label">{html.escape(label)}:</div> + <div class="detail-value">{html.escape(value)}</div> </div> """ for label, value in rows
tests/client/test_oauth_callback_xss.py+159 −0 added@@ -0,0 +1,159 @@ +"""Comprehensive XSS protection tests for OAuth callback HTML rendering.""" + +import pytest + +from fastmcp.client.oauth_callback import create_callback_html +from fastmcp.utilities.ui import ( + create_detail_box, + create_info_box, + create_page, + create_status_message, +) + + +def test_ui_create_page_escapes_title(): + """Test that page title is properly escaped.""" + xss_title = "<script>alert(1)</script>" + html = create_page("content", title=xss_title) + assert "<script>alert(1)</script>" in html + assert "<script>alert(1)</script>" not in html + + +def test_ui_create_status_message_escapes(): + """Test that status messages are properly escaped.""" + xss_message = "<img src=x onerror=alert(1)>" + html = create_status_message(xss_message) + assert "<img src=x onerror=alert(1)>" in html + assert "<img src=x onerror=alert(1)>" not in html + + +def test_ui_create_info_box_escapes(): + """Test that info box content is properly escaped.""" + xss_content = "<iframe src=javascript:alert(1)></iframe>" + html = create_info_box(xss_content) + assert "<iframe" in html + assert "<iframe src=javascript:alert(1)>" not in html + + +def test_ui_create_detail_box_escapes(): + """Test that detail box labels and values are properly escaped.""" + xss_label = '<script>alert("label")</script>' + xss_value = '<script>alert("value")</script>' + html = create_detail_box([(xss_label, xss_value)]) + assert "<script>" in html + assert '<script>alert("label")</script>' not in html + assert '<script>alert("value")</script>' not in html + + +def test_callback_html_escapes_error_message(): + """Test that XSS payloads in error messages are properly escaped.""" + xss_payload = "<img/src/onerror=alert(1)>" + html = create_callback_html(xss_payload, is_success=False) + + assert "<img/src/onerror=alert(1)>" in html + assert "<img/src/onerror=alert(1)>" not in html + + +def test_callback_html_escapes_server_url(): + """Test that XSS payloads in server_url are properly escaped.""" + xss_payload = "<script>alert(1)</script>" + html = create_callback_html("Success", is_success=True, server_url=xss_payload) + + assert "<script>alert(1)</script>" in html + assert "<script>alert(1)</script>" not in html + + +def test_callback_html_escapes_title(): + """Test that XSS payloads in title are properly escaped.""" + xss_payload = "<script>alert(document.domain)</script>" + html = create_callback_html("Success", title=xss_payload) + + assert "<script>alert(document.domain)</script>" in html + assert "<script>alert(document.domain)</script>" not in html + + +def test_callback_html_mixed_content(): + """Test that legitimate text mixed with XSS attempts is properly escaped.""" + mixed_payload = "Error: <img src=x onerror=alert(1)> occurred" + html = create_callback_html(mixed_payload, is_success=False) + + assert "<img src=x onerror=alert(1)>" in html + assert "Error:" in html + assert "occurred" in html + assert "<img src=x onerror=alert(1)>" not in html + + +def test_callback_html_event_handlers(): + """Test that event handler attributes are escaped.""" + xss_payload = '" onload="alert(1)' + html = create_callback_html(xss_payload, is_success=False) + + assert "" onload="alert(1)" in html + assert '" onload="alert(1)' not in html + + +def test_callback_html_special_characters(): + """Test that special HTML characters are properly escaped.""" + special_chars = "&<>\"'/" + html = create_callback_html(special_chars, is_success=False) + + assert "&" in html + assert "<" in html + assert ">" in html + assert """ in html + # Apostrophe gets escaped to ' by html.escape() + assert "'" in html + + +@pytest.mark.parametrize( + "xss_vector", + [ + "<img src=x onerror=alert(1)>", + "<script>alert(document.cookie)</script>", + "<iframe src=javascript:alert(1)>", + "<svg/onload=alert(1)>", + "<body onload=alert(1)>", + "<input onfocus=alert(1) autofocus>", + "<select onfocus=alert(1) autofocus>", + "<textarea onfocus=alert(1) autofocus>", + "<marquee onstart=alert(1)>", + "<div style=background:url('javascript:alert(1)')>", + ], +) +def test_common_xss_vectors(xss_vector: str): + """Test that common XSS attack vectors are properly escaped.""" + html = create_callback_html(xss_vector, is_success=False) + + # Should not contain the raw XSS vector + assert xss_vector not in html + + # Should contain escaped version (at least the < and > should be escaped) + assert "<" in html + assert ">" in html + + +def test_legitimate_content_still_works(): + """Ensure legitimate content is displayed correctly after escaping.""" + legitimate_message = "Authentication failed: Invalid credentials" + legitimate_url = "https://example.com:8080/mcp" + + # Error case + html = create_callback_html(legitimate_message, is_success=False) + assert legitimate_message in html + assert "Authentication failed" in html + + # Success case + html = create_callback_html("Success", is_success=True, server_url=legitimate_url) + assert legitimate_url in html + assert "Authentication successful" in html + + +def test_no_hardcoded_html_tags(): + """Verify that there are no hardcoded HTML tags that bypass escaping.""" + server_url = "test-server" + html = create_callback_html("Success", is_success=True, server_url=server_url) + + # Should not have <strong> tags around the server URL + assert f"<strong>{server_url}</strong>" not in html + # Should have the server URL displayed normally (escaped) + assert server_url in html
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-mxxr-jv3v-6pgcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-62800ghsaADVISORY
- github.com/jlowin/fastmcp/commit/2a20f54617a37213ed83894a8c2f0ac38a2e83a3ghsaWEB
- github.com/jlowin/fastmcp/pull/2090ghsaWEB
- github.com/jlowin/fastmcp/security/advisories/GHSA-mxxr-jv3v-6pgcghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.