From c55a1a0182445cf11d02c72514ea094c571a9d96 Mon Sep 17 00:00:00 2001 From: Auto Date: Thu, 5 Feb 2026 08:52:47 +0200 Subject: [PATCH] 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 --- server/routers/devserver.py | 112 +++++++-- server/services/dev_server_manager.py | 18 +- test_devserver_security.py | 319 ++++++++++++++++++++++++++ 3 files changed, 418 insertions(+), 31 deletions(-) create mode 100644 test_devserver_security.py diff --git a/server/routers/devserver.py b/server/routers/devserver.py index def413a..bc4029c 100644 --- a/server/routers/devserver.py +++ b/server/routers/devserver.py @@ -7,8 +7,8 @@ Uses project registry for path lookups and project_config for command detection. """ import logging -import sys import shlex +import sys from pathlib import Path from fastapi import APIRouter, HTTPException @@ -73,7 +73,20 @@ def get_project_dir(project_name: str) -> Path: return project_dir -ALLOWED_RUNNERS = {"npm", "pnpm", "yarn", "uvicorn", "python", "python3"} +ALLOWED_RUNNERS = { + "npm", "pnpm", "yarn", "npx", + "uvicorn", "python", "python3", + "flask", "poetry", + "cargo", "go", +} + +ALLOWED_NPM_SCRIPTS = {"dev", "start", "serve", "develop", "server", "preview"} + +# Allowed Python -m modules for dev servers +ALLOWED_PYTHON_MODULES = {"uvicorn", "flask", "gunicorn", "http.server"} + +BLOCKED_SHELLS = {"sh", "bash", "zsh", "cmd", "powershell", "pwsh", "cmd.exe"} + def validate_custom_command_strict(cmd: str) -> None: """ @@ -90,46 +103,85 @@ def validate_custom_command_strict(cmd: str) -> None: base = Path(argv[0]).name.lower() # Block direct shells / interpreters commonly used for command injection - if base in {"sh", "bash", "zsh", "cmd", "powershell", "pwsh"}: + if base in BLOCKED_SHELLS: raise ValueError(f"custom_command runner not allowed: {base}") if base not in ALLOWED_RUNNERS: - raise ValueError(f"custom_command runner not allowed: {base}") + raise ValueError( + f"custom_command runner not allowed: {base}. " + f"Allowed: {', '.join(sorted(ALLOWED_RUNNERS))}" + ) - # Block one-liner execution + # Block one-liner execution for python lowered = [a.lower() for a in argv] if base in {"python", "python3"}: if "-c" in lowered: raise ValueError("python -c is not allowed") - # Only allow: python -m uvicorn ... - if len(argv) < 3 or argv[1:3] != ["-m", "uvicorn"]: - raise ValueError("Only 'python -m uvicorn ...' is allowed") + if len(argv) >= 3 and argv[1] == "-m": + # Allow: python -m ... + if argv[2] not in ALLOWED_PYTHON_MODULES: + raise ValueError( + f"python -m {argv[2]} is not allowed. " + f"Allowed modules: {', '.join(sorted(ALLOWED_PYTHON_MODULES))}" + ) + elif len(argv) >= 2 and argv[1].endswith(".py"): + # Allow: python manage.py runserver, python app.py, etc. + pass + else: + raise ValueError( + "Python commands must use 'python -m ...' or 'python