Fix: URL-encode server name in Cursor deeplinks (#2369)

Server names with special characters (&, ?, #, etc.) were creating
malformed deeplink URLs. Now properly percent-encoded.
This commit is contained in:
Jeremiah Lowin 2025-11-04 08:26:57 -08:00 committed by GitHub
commit 80d10d23f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 51 additions and 8 deletions

View file

@ -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

View file

@ -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"),
("test<calc", "test%3Ccalc"),
("test>calc", "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."""