diff --git a/security.py b/security.py index 195aa92..0a22785 100644 --- a/security.py +++ b/security.py @@ -7,12 +7,17 @@ Uses an allowlist approach - only explicitly permitted commands can run. """ import os +import re import shlex from pathlib import Path from typing import Optional import yaml +# Regex pattern for valid pkill process names (no regex metacharacters allowed) +# Matches alphanumeric names with dots, underscores, and hyphens +VALID_PROCESS_NAME_PATTERN = re.compile(r"^[A-Za-z0-9._-]+$") + # Allowed commands for development tasks # Minimal set needed for the autonomous coding demo ALLOWED_COMMANDS = { @@ -474,9 +479,17 @@ def load_org_config() -> Optional[dict]: processes = config["pkill_processes"] if not isinstance(processes, list): return None + # Normalize and validate each process name against safe pattern + normalized = [] for proc in processes: - if not isinstance(proc, str) or proc.strip() == "": + if not isinstance(proc, str): return None + proc = proc.strip() + # Block empty strings and regex metacharacters + if not proc or not VALID_PROCESS_NAME_PATTERN.fullmatch(proc): + return None + normalized.append(proc) + config["pkill_processes"] = normalized return config @@ -536,9 +549,17 @@ def load_project_commands(project_dir: Path) -> Optional[dict]: processes = config["pkill_processes"] if not isinstance(processes, list): return None + # Normalize and validate each process name against safe pattern + normalized = [] for proc in processes: - if not isinstance(proc, str) or proc.strip() == "": + if not isinstance(proc, str): return None + proc = proc.strip() + # Block empty strings and regex metacharacters + if not proc or not VALID_PROCESS_NAME_PATTERN.fullmatch(proc): + return None + normalized.append(proc) + config["pkill_processes"] = normalized return config diff --git a/test_security.py b/test_security.py index 5bd1867..4bb6d24 100644 --- a/test_security.py +++ b/test_security.py @@ -809,6 +809,73 @@ pkill_processes: print(f" FAIL: pkill python should be allowed with org config: {result}") failed += 1 + # Test 9: Regex metacharacters should be rejected in pkill_processes + with tempfile.TemporaryDirectory() as tmphome: + with tempfile.TemporaryDirectory() as tmpproject: + with temporary_home(tmphome): + org_dir = Path(tmphome) / ".autocoder" + org_dir.mkdir() + org_config_path = org_dir / "config.yaml" + + # Try to register a regex pattern (should be rejected) + org_config_path.write_text("""version: 1 +pkill_processes: + - ".*" +""") + + config = load_org_config() + if config is None: + print(" PASS: Regex pattern '.*' rejected in pkill_processes") + passed += 1 + else: + print(" FAIL: Regex pattern '.*' should be rejected") + failed += 1 + + # Test 10: Valid process names with dots/underscores/hyphens should be accepted + with tempfile.TemporaryDirectory() as tmphome: + with tempfile.TemporaryDirectory() as tmpproject: + with temporary_home(tmphome): + org_dir = Path(tmphome) / ".autocoder" + org_dir.mkdir() + org_config_path = org_dir / "config.yaml" + + # Valid names with special chars + org_config_path.write_text("""version: 1 +pkill_processes: + - my-app + - app_server + - node.js +""") + + config = load_org_config() + if config is not None and config.get("pkill_processes") == ["my-app", "app_server", "node.js"]: + print(" PASS: Valid process names with dots/underscores/hyphens accepted") + passed += 1 + else: + print(f" FAIL: Valid process names should be accepted: {config}") + failed += 1 + + # Test 11: Names with spaces should be rejected + with tempfile.TemporaryDirectory() as tmphome: + with tempfile.TemporaryDirectory() as tmpproject: + with temporary_home(tmphome): + org_dir = Path(tmphome) / ".autocoder" + org_dir.mkdir() + org_config_path = org_dir / "config.yaml" + + org_config_path.write_text("""version: 1 +pkill_processes: + - "my app" +""") + + config = load_org_config() + if config is None: + print(" PASS: Process name with space rejected") + passed += 1 + else: + print(" FAIL: Process name with space should be rejected") + failed += 1 + return passed, failed