security: fix shell injection and add subprocess timeout

- Replace create_subprocess_shell with create_subprocess_exec
  in _try_install_dotnet_sdk() to prevent shell injection
- Add install_commands list to _detect_platform() returning
  safe argument lists for each platform
- Add 5-minute timeout to ilspy_wrapper._run_command() to
  prevent hanging on malicious/corrupted assemblies
This commit is contained in:
Ryan Malloy 2026-02-05 10:39:52 -07:00
parent 80a0a15cfc
commit f52790cec0
2 changed files with 85 additions and 29 deletions

View File

@ -71,7 +71,18 @@ class ILSpyWrapper:
) )
input_bytes = input_data.encode("utf-8") if input_data else None input_bytes = input_data.encode("utf-8") if input_data else None
stdout_bytes, stderr_bytes = await process.communicate(input=input_bytes)
# Timeout after 5 minutes to prevent hanging on malicious/corrupted assemblies
try:
stdout_bytes, stderr_bytes = await asyncio.wait_for(
process.communicate(input=input_bytes),
timeout=300.0 # 5 minutes
)
except asyncio.TimeoutError:
logger.warning(f"Command timed out after 5 minutes, killing process")
process.kill()
await process.wait() # Ensure process is cleaned up
return -1, "", "Command timed out after 5 minutes. The assembly may be corrupted or too complex."
stdout = stdout_bytes.decode("utf-8", errors="replace") if stdout_bytes else "" stdout = stdout_bytes.decode("utf-8", errors="replace") if stdout_bytes else ""
stderr = stderr_bytes.decode("utf-8", errors="replace") if stderr_bytes else "" stderr = stderr_bytes.decode("utf-8", errors="replace") if stderr_bytes else ""

View File

@ -81,13 +81,23 @@ async def _check_dotnet_tools() -> dict:
def _detect_platform() -> dict: def _detect_platform() -> dict:
"""Detect the platform and recommend the appropriate .NET SDK install command.""" """Detect the platform and recommend the appropriate .NET SDK install command.
Returns a dict with:
- system: OS name (linux, darwin, windows)
- distro: Distribution name if detected
- package_manager: Package manager name
- install_command: Human-readable command string (for display)
- install_commands: List of command arg lists for safe execution (no shell)
- needs_sudo: Whether installation requires elevated privileges
"""
system = platform.system().lower() system = platform.system().lower()
result = { result = {
"system": system, "system": system,
"distro": None, "distro": None,
"package_manager": None, "package_manager": None,
"install_command": None, "install_command": None,
"install_commands": None, # List of [arg, list, ...] for subprocess_exec
"needs_sudo": True, "needs_sudo": True,
} }
@ -99,39 +109,63 @@ def _detect_platform() -> dict:
if "arch" in os_release or "manjaro" in os_release or "endeavour" in os_release: if "arch" in os_release or "manjaro" in os_release or "endeavour" in os_release:
result["distro"] = "arch" result["distro"] = "arch"
result["package_manager"] = "pacman" result["package_manager"] = "pacman"
result["install_command"] = "sudo pacman -S dotnet-sdk" result["install_command"] = "sudo pacman -S --noconfirm dotnet-sdk"
result["install_commands"] = [
["sudo", "pacman", "-S", "--noconfirm", "dotnet-sdk"]
]
elif "ubuntu" in os_release or "debian" in os_release or "mint" in os_release: elif "ubuntu" in os_release or "debian" in os_release or "mint" in os_release:
result["distro"] = "debian" result["distro"] = "debian"
result["package_manager"] = "apt" result["package_manager"] = "apt"
result["install_command"] = "sudo apt update && sudo apt install -y dotnet-sdk-8.0" result["install_command"] = "sudo apt update && sudo apt install -y dotnet-sdk-8.0"
result["install_commands"] = [
["sudo", "apt", "update"],
["sudo", "apt", "install", "-y", "dotnet-sdk-8.0"],
]
elif "fedora" in os_release or "rhel" in os_release or "centos" in os_release: elif "fedora" in os_release or "rhel" in os_release or "centos" in os_release:
result["distro"] = "fedora" result["distro"] = "fedora"
result["package_manager"] = "dnf" result["package_manager"] = "dnf"
result["install_command"] = "sudo dnf install -y dotnet-sdk-8.0" result["install_command"] = "sudo dnf install -y dotnet-sdk-8.0"
result["install_commands"] = [
["sudo", "dnf", "install", "-y", "dotnet-sdk-8.0"]
]
elif "opensuse" in os_release or "suse" in os_release: elif "opensuse" in os_release or "suse" in os_release:
result["distro"] = "suse" result["distro"] = "suse"
result["package_manager"] = "zypper" result["package_manager"] = "zypper"
result["install_command"] = "sudo zypper install -y dotnet-sdk-8.0" result["install_command"] = "sudo zypper install -y dotnet-sdk-8.0"
result["install_commands"] = [
["sudo", "zypper", "install", "-y", "dotnet-sdk-8.0"]
]
except FileNotFoundError: except FileNotFoundError:
pass pass
# Fallback: check for common package managers # Fallback: check for common package managers
if result["install_command"] is None: if result["install_commands"] is None:
if shutil.which("pacman"): if shutil.which("pacman"):
result["package_manager"] = "pacman" result["package_manager"] = "pacman"
result["install_command"] = "sudo pacman -S dotnet-sdk" result["install_command"] = "sudo pacman -S --noconfirm dotnet-sdk"
result["install_commands"] = [
["sudo", "pacman", "-S", "--noconfirm", "dotnet-sdk"]
]
elif shutil.which("apt"): elif shutil.which("apt"):
result["package_manager"] = "apt" result["package_manager"] = "apt"
result["install_command"] = "sudo apt update && sudo apt install -y dotnet-sdk-8.0" result["install_command"] = "sudo apt update && sudo apt install -y dotnet-sdk-8.0"
result["install_commands"] = [
["sudo", "apt", "update"],
["sudo", "apt", "install", "-y", "dotnet-sdk-8.0"],
]
elif shutil.which("dnf"): elif shutil.which("dnf"):
result["package_manager"] = "dnf" result["package_manager"] = "dnf"
result["install_command"] = "sudo dnf install -y dotnet-sdk-8.0" result["install_command"] = "sudo dnf install -y dotnet-sdk-8.0"
result["install_commands"] = [
["sudo", "dnf", "install", "-y", "dotnet-sdk-8.0"]
]
elif system == "darwin": elif system == "darwin":
result["distro"] = "macos" result["distro"] = "macos"
if shutil.which("brew"): if shutil.which("brew"):
result["package_manager"] = "homebrew" result["package_manager"] = "homebrew"
result["install_command"] = "brew install dotnet-sdk" result["install_command"] = "brew install dotnet-sdk"
result["install_commands"] = [["brew", "install", "dotnet-sdk"]]
result["needs_sudo"] = False result["needs_sudo"] = False
else: else:
result["install_command"] = ( result["install_command"] = (
@ -143,10 +177,14 @@ def _detect_platform() -> dict:
if shutil.which("winget"): if shutil.which("winget"):
result["package_manager"] = "winget" result["package_manager"] = "winget"
result["install_command"] = "winget install Microsoft.DotNet.SDK.8" result["install_command"] = "winget install Microsoft.DotNet.SDK.8"
result["install_commands"] = [
["winget", "install", "Microsoft.DotNet.SDK.8", "--accept-source-agreements"]
]
result["needs_sudo"] = False result["needs_sudo"] = False
elif shutil.which("choco"): elif shutil.which("choco"):
result["package_manager"] = "chocolatey" result["package_manager"] = "chocolatey"
result["install_command"] = "choco install dotnet-sdk -y" result["install_command"] = "choco install dotnet-sdk -y"
result["install_commands"] = [["choco", "install", "dotnet-sdk", "-y"]]
else: else:
result["install_command"] = "Download from https://dotnet.microsoft.com/download" result["install_command"] = "Download from https://dotnet.microsoft.com/download"
result["needs_sudo"] = False result["needs_sudo"] = False
@ -163,44 +201,51 @@ async def _try_install_dotnet_sdk(ctx: Context | None = None) -> tuple[bool, str
"""Attempt to install the .NET SDK using the detected package manager. """Attempt to install the .NET SDK using the detected package manager.
Returns (success, message) tuple. Returns (success, message) tuple.
Security: Uses create_subprocess_exec with explicit argument lists
to avoid shell injection vulnerabilities.
""" """
platform_info = _detect_platform() platform_info = _detect_platform()
cmd = platform_info.get("install_command") commands = platform_info.get("install_commands")
display_cmd = platform_info.get("install_command", "")
if not cmd or "Download from" in cmd: if not commands:
return False, ( return False, (
f"Cannot auto-install .NET SDK on {platform_info['system']}.\n\n" f"Cannot auto-install .NET SDK on {platform_info['system']}.\n\n"
f"Please install manually: {cmd}" f"Please install manually: {display_cmd}"
) )
if platform_info.get("needs_sudo") and os.geteuid() != 0:
# We need sudo but aren't root - try anyway, it might prompt or fail gracefully
pass
if ctx: if ctx:
await ctx.info(f"Installing .NET SDK via {platform_info['package_manager']}...") await ctx.info(f"Installing .NET SDK via {platform_info['package_manager']}...")
all_output = []
try: try:
# Use shell=True to handle complex commands with && and sudo # Execute each command in sequence (e.g., apt update, then apt install)
proc = await asyncio.create_subprocess_shell( for cmd_args in commands:
cmd, if ctx:
await ctx.info(f"Running: {' '.join(cmd_args)}")
proc = await asyncio.create_subprocess_exec(
*cmd_args,
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
) )
stdout, stderr = await proc.communicate() stdout, stderr = await proc.communicate()
output = stdout.decode() + stderr.decode() output = stdout.decode() + stderr.decode()
all_output.append(f"$ {' '.join(cmd_args)}\n{output}")
if proc.returncode == 0: if proc.returncode != 0:
return True, f"✅ .NET SDK installed successfully via {platform_info['package_manager']}!"
else:
return False, ( return False, (
f"❌ Installation failed (exit code {proc.returncode}).\n\n" f"❌ Installation failed (exit code {proc.returncode}).\n\n"
f"Command: `{cmd}`\n\n" f"Command: `{' '.join(cmd_args)}`\n\n"
f"Output:\n```\n{output[-1000:]}\n```\n\n" f"Output:\n```\n{output[-1000:]}\n```\n\n"
"Try running the command manually with sudo if needed." "Try running the command manually with sudo if needed."
) )
return True, f"✅ .NET SDK installed successfully via {platform_info['package_manager']}!"
except Exception as e: except Exception as e:
return False, f"❌ Failed to run install command: {e}\n\nTry manually: `{cmd}`" return False, f"❌ Failed to run install command: {e}\n\nTry manually: `{display_cmd}`"
@mcp.tool() @mcp.tool()