@@ -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
122164class TestOpenDeeplink :
123165 """Test deeplink opening functionality."""
0 commit comments