mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-02-05 16:33:08 +00:00
Merge pull request #153 from syphonetic/master
Implemented RCE mitigation measures
This commit is contained in:
@@ -17,6 +17,7 @@ import re
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import shlex
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Awaitable, Callable, Literal, Set
|
||||
@@ -291,53 +292,47 @@ class DevServerProcessManager:
|
||||
Start the dev server as a subprocess.
|
||||
|
||||
Args:
|
||||
command: The shell command to run (e.g., "npm run dev")
|
||||
command: The command to run (e.g., "npm run dev")
|
||||
|
||||
Returns:
|
||||
Tuple of (success, message)
|
||||
"""
|
||||
if self.status == "running":
|
||||
# Already running?
|
||||
if self.process and self.status == "running":
|
||||
return False, "Dev server is already running"
|
||||
|
||||
# Lock check (prevents double-start)
|
||||
if not self._check_lock():
|
||||
return False, "Another dev server instance is already running for this project"
|
||||
return False, "Dev server already running (lock file present)"
|
||||
|
||||
# Validate that project directory exists
|
||||
if not self.project_dir.exists():
|
||||
return False, f"Project directory does not exist: {self.project_dir}"
|
||||
command = (command or "").strip()
|
||||
if not command:
|
||||
return False, "Empty dev server command"
|
||||
|
||||
# Defense-in-depth: validate command against security allowlist
|
||||
commands = extract_commands(command)
|
||||
if not commands:
|
||||
return False, "Could not parse command for security validation"
|
||||
# SECURITY: block shell operators/metacharacters (defense-in-depth)
|
||||
dangerous_ops = ["&&", "||", ";", "|", "`", "$("]
|
||||
if any(op in command for op in dangerous_ops):
|
||||
return False, "Shell operators are not allowed in dev server command"
|
||||
|
||||
allowed_commands, blocked_commands = get_effective_commands(self.project_dir)
|
||||
for cmd in commands:
|
||||
if cmd in blocked_commands:
|
||||
logger.warning("Blocked dev server command '%s' (in blocklist) for %s", cmd, self.project_name)
|
||||
return False, f"Command '{cmd}' is blocked and cannot be used as a dev server command"
|
||||
if not is_command_allowed(cmd, allowed_commands):
|
||||
logger.warning("Rejected dev server command '%s' (not in allowlist) for %s", cmd, self.project_name)
|
||||
return False, f"Command '{cmd}' is not in the allowed commands list"
|
||||
# Parse into argv and execute without shell
|
||||
argv = shlex.split(command, posix=(sys.platform != "win32"))
|
||||
if not argv:
|
||||
return False, "Empty dev server command"
|
||||
|
||||
self._command = command
|
||||
self._detected_url = None # Reset URL detection
|
||||
base = Path(argv[0]).name.lower()
|
||||
|
||||
# Defense-in-depth: reject direct shells/interpreters commonly used for injection
|
||||
if base in {"sh", "bash", "zsh", "cmd", "powershell", "pwsh"}:
|
||||
return False, f"Shell runner '{base}' is not allowed for dev server commands"
|
||||
|
||||
# Windows: use .cmd shims for Node package managers
|
||||
if sys.platform == "win32" and base in {"npm", "pnpm", "yarn", "npx"} and not argv[0].lower().endswith(".cmd"):
|
||||
argv[0] = argv[0] + ".cmd"
|
||||
|
||||
try:
|
||||
# Determine shell based on platform
|
||||
if sys.platform == "win32":
|
||||
# On Windows, use cmd.exe
|
||||
shell_cmd = ["cmd", "/c", command]
|
||||
else:
|
||||
# On Unix-like systems, use sh
|
||||
shell_cmd = ["sh", "-c", command]
|
||||
|
||||
# Start subprocess with piped stdout/stderr
|
||||
# stdin=DEVNULL prevents interactive dev servers from blocking on stdin
|
||||
# On Windows, use CREATE_NO_WINDOW to prevent console window from flashing
|
||||
if sys.platform == "win32":
|
||||
self.process = subprocess.Popen(
|
||||
shell_cmd,
|
||||
argv,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
@@ -346,23 +341,35 @@ class DevServerProcessManager:
|
||||
)
|
||||
else:
|
||||
self.process = subprocess.Popen(
|
||||
shell_cmd,
|
||||
argv,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=str(self.project_dir),
|
||||
)
|
||||
|
||||
self._create_lock()
|
||||
self.started_at = datetime.now()
|
||||
self.status = "running"
|
||||
self._command = command
|
||||
self.started_at = datetime.utcnow()
|
||||
self._detected_url = None
|
||||
|
||||
# Start output streaming task
|
||||
# Create lock once we have a PID
|
||||
self._create_lock()
|
||||
|
||||
# Start output streaming
|
||||
self.status = "running"
|
||||
self._output_task = asyncio.create_task(self._stream_output())
|
||||
|
||||
return True, f"Dev server started with PID {self.process.pid}"
|
||||
return True, "Dev server started"
|
||||
|
||||
except FileNotFoundError:
|
||||
self.status = "stopped"
|
||||
self.process = None
|
||||
self._remove_lock()
|
||||
return False, f"Command not found: {argv[0]}"
|
||||
except Exception as e:
|
||||
logger.exception("Failed to start dev server")
|
||||
self.status = "stopped"
|
||||
self.process = None
|
||||
self._remove_lock()
|
||||
return False, f"Failed to start dev server: {e}"
|
||||
|
||||
async def stop(self) -> tuple[bool, str]:
|
||||
|
||||
Reference in New Issue
Block a user