Skip to content

Commit 80d10d2

Browse files
authored
Fix: URL-encode server name in Cursor deeplinks (#2369)
Server names with special characters (&, ?, #, etc.) were creating malformed deeplink URLs. Now properly percent-encoded.
1 parent c6a9b3d commit 80d10d2

File tree

2 files changed

+51
-8
lines changed

2 files changed

+51
-8
lines changed

src/fastmcp/cli/install/cursor.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import sys
77
from pathlib import Path
88
from typing import Annotated
9-
from urllib.parse import urlparse
9+
from urllib.parse import quote, urlparse
1010

1111
import cyclopts
1212
from rich import print
@@ -38,8 +38,9 @@ def generate_cursor_deeplink(
3838
config_json = server_config.model_dump_json(exclude_none=True)
3939
config_b64 = base64.urlsafe_b64encode(config_json.encode()).decode()
4040

41-
# Generate the deeplink URL
42-
deeplink = f"cursor://anysphere.cursor-deeplink/mcp/install?name={server_name}&config={config_b64}"
41+
# Generate the deeplink URL with properly encoded server name
42+
encoded_name = quote(server_name, safe="")
43+
deeplink = f"cursor://anysphere.cursor-deeplink/mcp/install?name={encoded_name}&config={config_b64}"
4344

4445
return deeplink
4546

tests/cli/test_cursor.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ def test_generate_deeplink_special_characters(self):
6969
args=["run", "--with", "fastmcp", "fastmcp", "run", "server.py"],
7070
)
7171

72-
# Test with spaces and special chars in name
72+
# Test with spaces and special chars in name - should be URL encoded
7373
deeplink = generate_cursor_deeplink("my server (test)", server_config)
7474

75-
assert (
76-
"name=my%20server%20%28test%29" in deeplink
77-
or "name=my server (test)" in deeplink
78-
)
75+
# Spaces and parentheses must be URL-encoded
76+
assert "name=my%20server%20%28test%29" in deeplink
77+
# Ensure no unencoded version appears
78+
assert "name=my server (test)" not in deeplink
7979

8080
def test_generate_deeplink_empty_config(self):
8181
"""Test deeplink generation with minimal config."""
@@ -118,6 +118,48 @@ def test_generate_deeplink_complex_args(self):
118118
assert "--with-editable" in config_data["args"]
119119
assert "server.py:CustomServer" in config_data["args"]
120120

121+
def test_generate_deeplink_url_injection_protection(self):
122+
"""Test that special characters in server name are properly URL-encoded to prevent injection."""
123+
server_config = StdioMCPServer(
124+
command="python",
125+
args=["server.py"],
126+
)
127+
128+
# Test the PoC case from the security advisory
129+
deeplink = generate_cursor_deeplink("test&calc", server_config)
130+
131+
# The & should be encoded as %26, preventing it from being interpreted as a query parameter separator
132+
assert "name=test%26calc" in deeplink
133+
assert "name=test&calc" not in deeplink
134+
135+
# Verify the URL structure is intact
136+
assert deeplink.startswith("cursor://anysphere.cursor-deeplink/mcp/install?")
137+
assert deeplink.count("&") == 1 # Only one & between name and config parameters
138+
139+
# Test other potentially dangerous characters
140+
dangerous_names = [
141+
("test|calc", "test%7Ccalc"),
142+
("test;calc", "test%3Bcalc"),
143+
("test<calc", "test%3Ccalc"),
144+
("test>calc", "test%3Ecalc"),
145+
("test`calc", "test%60calc"),
146+
("test$calc", "test%24calc"),
147+
("test'calc", "test%27calc"),
148+
('test"calc', "test%22calc"),
149+
("test calc", "test%20calc"),
150+
("test#anchor", "test%23anchor"),
151+
("test?query=val", "test%3Fquery%3Dval"),
152+
]
153+
154+
for dangerous_name, expected_encoded in dangerous_names:
155+
deeplink = generate_cursor_deeplink(dangerous_name, server_config)
156+
assert f"name={expected_encoded}" in deeplink, (
157+
f"Failed to encode {dangerous_name}"
158+
)
159+
# Ensure no unencoded special chars that could break URL structure
160+
name_part = deeplink.split("name=")[1].split("&")[0]
161+
assert name_part == expected_encoded
162+
121163

122164
class TestOpenDeeplink:
123165
"""Test deeplink opening functionality."""

0 commit comments

Comments
 (0)