diff --git a/src/fastmcp/cli/install/shared.py b/src/fastmcp/cli/install/shared.py index fe980dce6..ce35bd886 100644 --- a/src/fastmcp/cli/install/shared.py +++ b/src/fastmcp/cli/install/shared.py @@ -2,6 +2,7 @@ import json import os +import re import subprocess import sys from pathlib import Path @@ -17,6 +18,26 @@ from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystem logger = get_logger(__name__) +# Server names are passed as subprocess arguments to CLI tools like `claude` +# and `gemini`. On Windows these may resolve to .cmd/.bat wrappers that run +# through cmd.exe, where shell metacharacters (& | ; etc.) in arguments can +# cause command injection. Restrict names to safe characters. +_SAFE_NAME_RE = re.compile(r"^[\w\-. ]+$") + + +def validate_server_name(name: str) -> str: + """Validate that a server name is safe for use as a subprocess argument. + + Raises SystemExit if the name contains shell metacharacters. + """ + if not _SAFE_NAME_RE.match(name): + print( + f"[red]Invalid server name '[bold]{name}[/bold]': " + "names may only contain letters, numbers, hyphens, underscores, dots, and spaces.[/red]" + ) + sys.exit(1) + return name + def parse_env_var(env_var: str) -> tuple[str, str]: """Parse environment variable string in format KEY=VALUE.""" @@ -123,6 +144,8 @@ async def process_common_args( ) name = file.stem + validate_server_name(name) + # Process environment variables if provided env_dict: dict[str, str] | None = None if env_file or env_vars: diff --git a/tests/cli/test_install.py b/tests/cli/test_install.py index 6b354c9b7..9dca455fa 100644 --- a/tests/cli/test_install.py +++ b/tests/cli/test_install.py @@ -1,6 +1,9 @@ from pathlib import Path +import pytest + from fastmcp.cli.install import install_app +from fastmcp.cli.install.shared import validate_server_name from fastmcp.cli.install.stdio import install_stdio @@ -455,3 +458,37 @@ class TestInstallCommandParsing: command, bound, _ = install_app.parse_args(cmd_args) assert command is not None assert str(bound.arguments["project"]) == str(Path("/path/to/project")) + + +class TestServerNameValidation: + """Test server name validation rejects shell metacharacters.""" + + @pytest.mark.parametrize( + "name", + [ + "my-server", + "my_server", + "My Server", + "server.v2", + "test123", + ], + ) + def test_valid_names(self, name: str): + assert validate_server_name(name) == name + + @pytest.mark.parametrize( + "name", + [ + "test&calc", + "test|whoami", + "test;ls", + "test$(id)", + "test`id`", + 'test"quoted', + "test>file", + "test