From 80d10d23f4efe907f37ce834595c54fc87a14ac3 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 4 Nov 2025 08:26:57 -0800 Subject: [PATCH] Fix: URL-encode server name in Cursor deeplinks (#2369) Server names with special characters (&, ?, #, etc.) were creating malformed deeplink URLs. Now properly percent-encoded. --- src/fastmcp/cli/install/cursor.py | 7 +++-- tests/cli/test_cursor.py | 52 ++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/fastmcp/cli/install/cursor.py b/src/fastmcp/cli/install/cursor.py index d8cf62859..de7fb6037 100644 --- a/src/fastmcp/cli/install/cursor.py +++ b/src/fastmcp/cli/install/cursor.py @@ -6,7 +6,7 @@ import subprocess import sys from pathlib import Path from typing import Annotated -from urllib.parse import urlparse +from urllib.parse import quote, urlparse import cyclopts from rich import print @@ -38,8 +38,9 @@ def generate_cursor_deeplink( config_json = server_config.model_dump_json(exclude_none=True) config_b64 = base64.urlsafe_b64encode(config_json.encode()).decode() - # Generate the deeplink URL - deeplink = f"cursor://anysphere.cursor-deeplink/mcp/install?name={server_name}&config={config_b64}" + # Generate the deeplink URL with properly encoded server name + encoded_name = quote(server_name, safe="") + deeplink = f"cursor://anysphere.cursor-deeplink/mcp/install?name={encoded_name}&config={config_b64}" return deeplink diff --git a/tests/cli/test_cursor.py b/tests/cli/test_cursor.py index ced1cfa62..7e63e7b9f 100644 --- a/tests/cli/test_cursor.py +++ b/tests/cli/test_cursor.py @@ -69,13 +69,13 @@ class TestCursorDeeplinkGeneration: args=["run", "--with", "fastmcp", "fastmcp", "run", "server.py"], ) - # Test with spaces and special chars in name + # Test with spaces and special chars in name - should be URL encoded deeplink = generate_cursor_deeplink("my server (test)", server_config) - assert ( - "name=my%20server%20%28test%29" in deeplink - or "name=my server (test)" in deeplink - ) + # Spaces and parentheses must be URL-encoded + assert "name=my%20server%20%28test%29" in deeplink + # Ensure no unencoded version appears + assert "name=my server (test)" not in deeplink def test_generate_deeplink_empty_config(self): """Test deeplink generation with minimal config.""" @@ -118,6 +118,48 @@ class TestCursorDeeplinkGeneration: assert "--with-editable" in config_data["args"] assert "server.py:CustomServer" in config_data["args"] + def test_generate_deeplink_url_injection_protection(self): + """Test that special characters in server name are properly URL-encoded to prevent injection.""" + server_config = StdioMCPServer( + command="python", + args=["server.py"], + ) + + # Test the PoC case from the security advisory + deeplink = generate_cursor_deeplink("test&calc", server_config) + + # The & should be encoded as %26, preventing it from being interpreted as a query parameter separator + assert "name=test%26calc" in deeplink + assert "name=test&calc" not in deeplink + + # Verify the URL structure is intact + assert deeplink.startswith("cursor://anysphere.cursor-deeplink/mcp/install?") + assert deeplink.count("&") == 1 # Only one & between name and config parameters + + # Test other potentially dangerous characters + dangerous_names = [ + ("test|calc", "test%7Ccalc"), + ("test;calc", "test%3Bcalc"), + ("testcalc", "test%3Ecalc"), + ("test`calc", "test%60calc"), + ("test$calc", "test%24calc"), + ("test'calc", "test%27calc"), + ('test"calc', "test%22calc"), + ("test calc", "test%20calc"), + ("test#anchor", "test%23anchor"), + ("test?query=val", "test%3Fquery%3Dval"), + ] + + for dangerous_name, expected_encoded in dangerous_names: + deeplink = generate_cursor_deeplink(dangerous_name, server_config) + assert f"name={expected_encoded}" in deeplink, ( + f"Failed to encode {dangerous_name}" + ) + # Ensure no unencoded special chars that could break URL structure + name_part = deeplink.split("name=")[1].split("&")[0] + assert name_part == expected_encoded + class TestOpenDeeplink: """Test deeplink opening functionality."""