mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 14:04:18 +02:00
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:
parent
5499cda9d0
commit
0e97a261cf
2 changed files with 57 additions and 18 deletions
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue