From d1dac1383d81edaf851c0f56c44c18eaf734689b Mon Sep 17 00:00:00 2001 From: Marian Paul Date: Thu, 22 Jan 2026 12:40:31 +0100 Subject: [PATCH] security: prevent bare wildcard '*' from matching all commands Add validation to reject bare wildcards for security: - matches_pattern(): return False if pattern == '*' - validate_project_command(): reject name == '*' with clear error - Added 4 new tests for bare wildcard rejection This prevents a config with from matching every command, which would be a major security risk. Tests: 140 unit tests passing (added 4 bare wildcard tests) --- security.py | 11 +++++++++++ test_security.py | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/security.py b/security.py index 0b8fd57..a73a2e9 100644 --- a/security.py +++ b/security.py @@ -371,6 +371,10 @@ def matches_pattern(command: str, pattern: str) -> bool: Returns: True if command matches pattern """ + # Reject bare wildcards - security measure to prevent matching everything + if pattern == "*": + return False + # Exact match if command == pattern: return True @@ -378,6 +382,9 @@ def matches_pattern(command: str, pattern: str) -> bool: # Prefix wildcard (e.g., "swift*" matches "swiftc", "swiftlint") if pattern.endswith("*"): prefix = pattern[:-1] + # Also reject if prefix is empty (would be bare "*") + if not prefix: + return False return command.startswith(prefix) # Local script paths (./scripts/build.sh matches build.sh) @@ -524,6 +531,10 @@ def validate_project_command(cmd_config: dict) -> tuple[bool, str]: if not isinstance(name, str) or not name: return False, "Command name must be a non-empty string" + # Reject bare wildcard - security measure to prevent matching all commands + if name == "*": + return False, "Bare wildcard '*' is not allowed (security risk: matches all commands)" + # Check if command is in the blocklist or dangerous commands base_cmd = os.path.basename(name.rstrip("*")) if base_cmd in BLOCKED_COMMANDS: diff --git a/test_security.py b/test_security.py index 27a1521..3af10cf 100644 --- a/test_security.py +++ b/test_security.py @@ -178,6 +178,11 @@ def test_pattern_matching(): ("swift", "swift*", True, "swift matches swift*"), ("npm", "swift*", False, "npm doesn't match swift*"), + # Bare wildcard (security: should NOT match anything) + ("npm", "*", False, "bare wildcard doesn't match npm"), + ("sudo", "*", False, "bare wildcard doesn't match sudo"), + ("anything", "*", False, "bare wildcard doesn't match anything"), + # Local script paths ("build.sh", "./scripts/build.sh", True, "script name matches path"), ("./scripts/build.sh", "./scripts/build.sh", True, "exact script path"), @@ -293,6 +298,9 @@ def test_command_validation(): ({"name": ""}, False, "empty name"), ({"name": 123}, False, "non-string name"), + # Security: Bare wildcard not allowed + ({"name": "*"}, False, "bare wildcard rejected"), + # Blocklisted commands ({"name": "sudo"}, False, "blocklisted sudo"), ({"name": "shutdown"}, False, "blocklisted shutdown"),