fix: harden dev server RCE mitigations from PR #153

Address security gaps and improve validation in the dev server command
execution path introduced by PR #153:

Security fixes (critical):
- Add missing shell metacharacters to dangerous_ops blocklist: single &
  (Windows cmd.exe command separator), >, <, ^, %, \n, \r
- The single & gap was a confirmed RCE bypass on Windows where .cmd
  files are always executed via cmd.exe even with shell=False (CPython
  limitation documented in issue #77696)
- Apply validate_custom_command_strict at /start endpoint for
  defense-in-depth against config file tampering

Validation improvements:
- Fix uvicorn --flag=value syntax (split on = before comparing)
- Expand Python support: Django (manage.py), Flask, custom .py scripts
- Add runners: flask, poetry, cargo, go, npx
- Expand npm script allowlist: serve, develop, server, preview
- Reorder PATCH /config validation to run strict check first (fail fast)
- Extract constants: ALLOWED_NPM_SCRIPTS, ALLOWED_PYTHON_MODULES,
  BLOCKED_SHELLS for reuse and testability

Cleanup:
- Remove unused security.py imports from dev_server_manager.py
- Fix deprecated datetime.utcnow() -> datetime.now(timezone.utc)
- Remove unnecessary _remove_lock() in exception handlers where lock
  was never created (Popen failure path)

Tests:
- Add test_devserver_security.py with 78 tests covering valid commands,
  blocked shells, blocked commands, injection attempts, dangerous_ops
  blocking, and constant verification

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-02-05 08:52:47 +02:00
parent 75766a433a
commit c55a1a0182
3 changed files with 418 additions and 31 deletions

View File

@@ -14,18 +14,17 @@ This is a simplified version of AgentProcessManager, tailored for dev servers:
import asyncio
import logging
import re
import shlex
import subprocess
import sys
import threading
import shlex
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path
from typing import Awaitable, Callable, Literal, Set
import psutil
from registry import list_registered_projects
from security import extract_commands, get_effective_commands, is_command_allowed
from server.utils.process_utils import kill_process_tree
logger = logging.getLogger(__name__)
@@ -310,9 +309,16 @@ class DevServerProcessManager:
return False, "Empty dev server command"
# SECURITY: block shell operators/metacharacters (defense-in-depth)
dangerous_ops = ["&&", "||", ";", "|", "`", "$("]
# NOTE: On Windows, .cmd/.bat files are executed via cmd.exe even with
# shell=False (CPython limitation), so metacharacter blocking is critical.
# Single & is a cmd.exe command separator, ^ is cmd escape, % enables
# environment variable expansion, > < enable redirection.
dangerous_ops = ["&&", "||", ";", "|", "`", "$(", "&", ">", "<", "^", "%"]
if any(op in command for op in dangerous_ops):
return False, "Shell operators are not allowed in dev server command"
# Block newline injection (cmd.exe interprets newlines as command separators)
if "\n" in command or "\r" in command:
return False, "Newlines are not allowed in dev server command"
# Parse into argv and execute without shell
argv = shlex.split(command, posix=(sys.platform != "win32"))
@@ -349,7 +355,7 @@ class DevServerProcessManager:
)
self._command = command
self.started_at = datetime.utcnow()
self.started_at = datetime.now(timezone.utc)
self._detected_url = None
# Create lock once we have a PID
@@ -364,12 +370,10 @@ class DevServerProcessManager:
except FileNotFoundError:
self.status = "stopped"
self.process = None
self._remove_lock()
return False, f"Command not found: {argv[0]}"
except Exception as e:
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]: