Security: Validate Cursor deeplink URLs and use safer Windows API (#2348)

* 🤖 Security: Validate Cursor deeplink URLs and replace cmd.exe on Windows

- Add URL scheme validation to reject non-cursor:// URLs
- Replace subprocess cmd.exe call with os.startfile() on Windows
- Add tests for scheme validation and error handling

* 🤖 Fix tests for cross-platform deeplink validation
This commit is contained in:
Cole Murray 2025-11-02 16:49:07 -08:00 committed by GitHub
commit 0e97a261cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 57 additions and 18 deletions

View file

@ -1,10 +1,12 @@
"""Cursor integration for FastMCP install using Cyclopts."""
import base64
import os
import subprocess
import sys
from pathlib import Path
from typing import Annotated
from urllib.parse import urlparse
import cyclopts
from rich import print
@ -51,17 +53,20 @@ def open_deeplink(deeplink: str) -> bool:
Returns:
True if the command succeeded, False otherwise
"""
parsed = urlparse(deeplink)
if parsed.scheme != "cursor":
logger.warning(f"Invalid deeplink scheme: {parsed.scheme}")
return False
try:
if sys.platform == "darwin": # macOS
subprocess.run(["open", deeplink], check=True, capture_output=True)
elif sys.platform == "win32": # Windows
subprocess.run(
["cmd", "/c", "start", deeplink], check=True, capture_output=True
)
os.startfile(deeplink)
else: # Linux and others
subprocess.run(["xdg-open", deeplink], check=True, capture_output=True)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return False

View file

@ -135,18 +135,16 @@ class TestOpenDeeplink:
["open", "cursor://test"], check=True, capture_output=True
)
@patch("subprocess.run")
def test_open_deeplink_windows(self, mock_run):
def test_open_deeplink_windows(self):
"""Test opening deeplink on Windows."""
with patch("sys.platform", "win32"):
mock_run.return_value = Mock(returncode=0)
with patch(
"fastmcp.cli.install.cursor.os.startfile", create=True
) as mock_startfile:
result = open_deeplink("cursor://test")
result = open_deeplink("cursor://test")
assert result is True
mock_run.assert_called_once_with(
["cmd", "/c", "start", "cursor://test"], check=True, capture_output=True
)
assert result is True
mock_startfile.assert_called_once_with("cursor://test")
@patch("subprocess.run")
def test_open_deeplink_linux(self, mock_run):
@ -166,21 +164,57 @@ class TestOpenDeeplink:
"""Test handling of deeplink opening failure."""
import subprocess
mock_run.side_effect = subprocess.CalledProcessError(1, ["open"])
with patch("sys.platform", "darwin"):
mock_run.side_effect = subprocess.CalledProcessError(1, ["open"])
result = open_deeplink("cursor://test")
result = open_deeplink("cursor://test")
assert result is False
assert result is False
@patch("subprocess.run")
def test_open_deeplink_command_not_found(self, mock_run):
"""Test handling when open command is not found."""
mock_run.side_effect = FileNotFoundError()
with patch("sys.platform", "darwin"):
mock_run.side_effect = FileNotFoundError()
result = open_deeplink("cursor://test")
result = open_deeplink("cursor://test")
assert result is False
def test_open_deeplink_invalid_scheme(self):
"""Test that non-cursor:// URLs are rejected."""
result = open_deeplink("http://malicious.com")
assert result is False
result = open_deeplink("https://example.com")
assert result is False
result = open_deeplink("file:///etc/passwd")
assert result is False
def test_open_deeplink_valid_cursor_scheme(self):
"""Test that cursor:// URLs are accepted."""
with patch("sys.platform", "darwin"):
with patch("subprocess.run") as mock_run:
mock_run.return_value = Mock(returncode=0)
result = open_deeplink("cursor://anysphere.cursor-deeplink/mcp/install")
assert result is True
def test_open_deeplink_empty_url(self):
"""Test handling of empty URL."""
result = open_deeplink("")
assert result is False
def test_open_deeplink_windows_oserror(self):
"""Test handling of OSError on Windows."""
with patch("sys.platform", "win32"):
with patch(
"fastmcp.cli.install.cursor.os.startfile", create=True
) as mock_startfile:
mock_startfile.side_effect = OSError("File not found")
result = open_deeplink("cursor://test")
assert result is False
class TestInstallCursor:
"""Test cursor installation functionality."""