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)
This commit is contained in:
Marian Paul
2026-01-22 12:40:31 +01:00
parent a9a0fcd865
commit d1dac1383d
2 changed files with 19 additions and 0 deletions

View File

@@ -371,6 +371,10 @@ def matches_pattern(command: str, pattern: str) -> bool:
Returns: Returns:
True if command matches pattern True if command matches pattern
""" """
# Reject bare wildcards - security measure to prevent matching everything
if pattern == "*":
return False
# Exact match # Exact match
if command == pattern: if command == pattern:
return True return True
@@ -378,6 +382,9 @@ def matches_pattern(command: str, pattern: str) -> bool:
# Prefix wildcard (e.g., "swift*" matches "swiftc", "swiftlint") # Prefix wildcard (e.g., "swift*" matches "swiftc", "swiftlint")
if pattern.endswith("*"): if pattern.endswith("*"):
prefix = pattern[:-1] prefix = pattern[:-1]
# Also reject if prefix is empty (would be bare "*")
if not prefix:
return False
return command.startswith(prefix) return command.startswith(prefix)
# Local script paths (./scripts/build.sh matches build.sh) # 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: if not isinstance(name, str) or not name:
return False, "Command name must be a non-empty string" 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 # Check if command is in the blocklist or dangerous commands
base_cmd = os.path.basename(name.rstrip("*")) base_cmd = os.path.basename(name.rstrip("*"))
if base_cmd in BLOCKED_COMMANDS: if base_cmd in BLOCKED_COMMANDS:

View File

@@ -178,6 +178,11 @@ def test_pattern_matching():
("swift", "swift*", True, "swift matches swift*"), ("swift", "swift*", True, "swift matches swift*"),
("npm", "swift*", False, "npm doesn't match 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 # Local script paths
("build.sh", "./scripts/build.sh", True, "script name matches path"), ("build.sh", "./scripts/build.sh", True, "script name matches path"),
("./scripts/build.sh", "./scripts/build.sh", True, "exact script path"), ("./scripts/build.sh", "./scripts/build.sh", True, "exact script path"),
@@ -293,6 +298,9 @@ def test_command_validation():
({"name": ""}, False, "empty name"), ({"name": ""}, False, "empty name"),
({"name": 123}, False, "non-string name"), ({"name": 123}, False, "non-string name"),
# Security: Bare wildcard not allowed
({"name": "*"}, False, "bare wildcard rejected"),
# Blocklisted commands # Blocklisted commands
({"name": "sudo"}, False, "blocklisted sudo"), ({"name": "sudo"}, False, "blocklisted sudo"),
({"name": "shutdown"}, False, "blocklisted shutdown"), ({"name": "shutdown"}, False, "blocklisted shutdown"),