fix(scripts): prevent command injection in check-quality-gates.py
CI / benchmark-publish (pull_request) Has been skipped
CI / push-validation (pull_request) Successful in 44s
CI / lint (pull_request) Failing after 1m16s
CI / helm (pull_request) Successful in 52s
CI / benchmark-regression (pull_request) Failing after 1m19s
CI / build (pull_request) Successful in 1m12s
CI / quality (pull_request) Successful in 1m43s
CI / typecheck (pull_request) Successful in 1m50s
CI / security (pull_request) Successful in 2m3s
CI / unit_tests (pull_request) Failing after 3m16s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 4m8s
CI / e2e_tests (pull_request) Successful in 4m14s
CI / status-check (pull_request) Failing after 4s

Add validate_path() function to sanitize subprocess path arguments and reject

dangerous shell metacharacters (; | & $ ` < >) and path traversal (..).

Update all quality gate checks to use validated paths. Explicitly set shell=False on

all subprocess.run calls.
This commit is contained in:
2026-05-08 15:51:56 +00:00
parent a79d22642a
commit fe94e54155
3 changed files with 304 additions and 6 deletions
@@ -0,0 +1,78 @@
Feature: Quality gates script prevents command injection attacks
As a security-conscious developer
I want the quality gates script to validate all path arguments
So that command injection attacks cannot be executed through path manipulation
Background:
Given the quality gates script is available
And the project root is set correctly
Scenario: Valid paths are accepted
When I validate the path "src/cleveragents"
Then the validation should succeed
And the returned path should be absolute
Scenario: Empty paths are rejected
When I validate the path ""
Then the validation should fail
And the path validation error should contain "Path cannot be empty"
Scenario: Paths with path traversal are rejected
When I validate the path "src/../../../etc/passwd"
Then the validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with semicolon are rejected
When I validate the path "src/cleveragents;rm -rf /"
Then the validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with pipe are rejected
When I validate the path "src/cleveragents|cat /etc/passwd"
Then the validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with ampersand are rejected
When I validate the path "src/cleveragents&whoami"
Then the validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with dollar sign are rejected
When I validate the path "src/cleveragents$(whoami)"
Then the validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with backticks are rejected
When I validate the path "src/cleveragents`whoami`"
Then the validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with parentheses are rejected
When I validate the path "src/cleveragents(whoami)"
Then the validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with angle brackets are rejected
When I validate the path "src/cleveragents<file"
Then the validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with newlines are rejected
When I validate the path "src/cleveragents\nmalicious"
Then the validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Security check uses validated paths
When I run the security check
Then the subprocess should be called with shell=False
And all path arguments should be validated
Scenario: Dead code check uses validated paths
When I run the dead code check
Then the subprocess should be called with shell=False
And all path arguments should be validated
Scenario: Complexity check uses validated paths
When I run the complexity check
Then the subprocess should be called with shell=False
And all path arguments should be validated
@@ -0,0 +1,132 @@
"""Step definitions for quality gates command injection tests."""
import importlib.util
import os
from pathlib import Path
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
def _load_quality_gates_module():
"""Load the check-quality-gates.py script as a module via importlib."""
spec = importlib.util.spec_from_file_location(
"check_quality_gates", "scripts/check-quality-gates.py"
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
_qg = _load_quality_gates_module()
@given("the quality gates script is available")
def step_script_available(context: Context) -> None:
"""Verify the quality gates script is available."""
script_path = Path("scripts/check-quality-gates.py")
assert script_path.exists(), f"Script {script_path} does not exist"
@given("the project root is set correctly")
def step_project_root_set(context: Context) -> None:
"""Set up the project root context."""
context.original_cwd = os.getcwd()
@when('I validate the path "{path_str}"')
def step_validate_path(context: Context, path_str: str) -> None:
"""Validate a path string."""
context.path_str = path_str
context.validation_error = None
context.validated_path = None
try:
context.validated_path = _qg.validate_path(path_str)
except ValueError as e:
context.validation_error = str(e)
@then("the validation should succeed")
def step_validation_success(context: Context) -> None:
"""Assert that validation succeeded."""
assert context.validation_error is None, (
f"Expected validation to succeed but got: {context.validation_error}"
)
assert context.validated_path is not None, "Expected a validated path"
@then("the validation should fail")
def step_validation_fail(context: Context) -> None:
"""Assert that validation failed."""
assert context.validation_error is not None, (
"Expected validation to fail but it succeeded"
)
@then("the returned path should be absolute")
def step_path_is_absolute(context: Context) -> None:
"""Assert that the returned path is absolute."""
assert context.validated_path is not None, "No validated path available"
assert context.validated_path.is_absolute(), (
f"Expected absolute path but got: {context.validated_path}"
)
@then('the path validation error should contain "{text}"')
def step_error_contains_text(context: Context, text: str) -> None:
"""Assert that the path validation error message contains specific text."""
assert context.validation_error is not None, "No error message available"
assert text.lower() in context.validation_error.lower(), (
f"Expected error to contain '{text}' but got: {context.validation_error}"
)
@when("I run the security check")
def step_run_security_check(context: Context) -> None:
"""Run the security check function."""
mock_result = MagicMock(returncode=0, stdout='{"results": []}', stderr="")
with patch.object(_qg.subprocess, "run", return_value=mock_result) as mock_run:
context.security_result = _qg.check_security()
context.mock_run = mock_run
@when("I run the dead code check")
def step_run_dead_code_check(context: Context) -> None:
"""Run the dead code check function."""
mock_result = MagicMock(returncode=0, stdout="", stderr="")
with patch.object(_qg.subprocess, "run", return_value=mock_result) as mock_run:
context.dead_code_result = _qg.check_dead_code()
context.mock_run = mock_run
@when("I run the complexity check")
def step_run_complexity_check(context: Context) -> None:
"""Run the complexity check function."""
mock_result = MagicMock(returncode=0, stdout="{}", stderr="")
with patch.object(_qg.subprocess, "run", return_value=mock_result) as mock_run:
context.complexity_result = _qg.check_complexity()
context.mock_run = mock_run
@then("the subprocess should be called with shell=False")
def step_subprocess_shell_false(context: Context) -> None:
"""Assert that subprocess.run was called with shell=False."""
assert hasattr(context, "mock_run"), "No mock_run available"
for call in context.mock_run.call_args_list:
kwargs = (
call.kwargs
if hasattr(call, "kwargs")
else (call[1] if len(call) > 1 else {})
)
assert kwargs.get("shell") is False, (
f"Expected shell=False but got: {kwargs.get('shell')}"
)
@then("all path arguments should be validated")
def step_all_paths_validated(context: Context) -> None:
"""Assert that all path arguments were validated."""
assert hasattr(context, "mock_run"), "No mock_run available"
assert context.mock_run.called, "subprocess.run was not called"
+94 -6
View File
@@ -5,7 +5,7 @@ Aggregates results from multiple quality tools and produces a summary
report. Returns non-zero exit code if any critical gate fails.
Usage:
python scripts/check-quality-gates.py [--coverage-min 85] [--complexity-max 10]
python scripts/check-quality-gates.py [--coverage-min 85] [--complexity-max F]
Gates checked:
1. Coverage >= threshold (default 85%)
@@ -22,6 +22,69 @@ import sys
from pathlib import Path
def validate_path(path_str: str, project_root: Path | None = None) -> Path:
"""Validate and sanitize a path argument to prevent command injection.
Checks for dangerous shell metacharacters and path traversal patterns.
Rejects paths that contain characters that could be exploited for
command injection when passed as subprocess arguments.
Args:
path_str: The path string to validate
project_root: Optional project root for relative path validation
Returns:
A validated, resolved Path object
Raises:
ValueError: If the path is invalid or contains dangerous patterns
"""
if not path_str:
raise ValueError("Path cannot be empty")
# Check for dangerous patterns that could be used for injection
dangerous_patterns = [
"..", # Path traversal
";", # Command separator
"|", # Pipe operator
"&", # Background/AND operator
"$", # Variable/command substitution
"`", # Backtick command substitution (deprecated)
"(", # Subshell grouping start
")", # Subshell grouping end
"<", # Input redirection
">", # Output redirection
"\n", # Newline injection
"\r", # Carriage return injection
]
for pattern in dangerous_patterns:
if pattern in path_str:
raise ValueError(f"Path contains dangerous pattern: {pattern}")
# Convert to Path object
path_obj = Path(path_str)
# Resolve to absolute path to prevent traversal
try:
resolved = path_obj.resolve()
except (OSError, RuntimeError) as e:
raise ValueError(f"Cannot resolve path: {e}") from e
# If project_root is provided, ensure path is within it
if project_root is not None:
project_root_resolved = project_root.resolve()
try:
resolved.relative_to(project_root_resolved)
except ValueError as e:
msg = (
f"Path {resolved} is outside project root "
f"{project_root_resolved}"
)
raise ValueError(msg) from e
return resolved
def check_coverage(min_coverage: float) -> tuple[bool, str]:
"""Check test coverage meets minimum threshold."""
result = subprocess.run(
@@ -29,6 +92,7 @@ def check_coverage(min_coverage: float) -> tuple[bool, str]:
capture_output=True,
text=True,
check=False,
shell=False,
)
if result.returncode != 0:
return False, f"Coverage command failed: {result.stderr.strip()}"
@@ -50,6 +114,7 @@ def check_typecheck() -> tuple[bool, str]:
capture_output=True,
text=True,
check=False,
shell=False,
)
try:
data = json.loads(result.stdout)
@@ -66,13 +131,19 @@ def check_typecheck() -> tuple[bool, str]:
def check_security() -> tuple[bool, str]:
"""Check bandit finds no high-severity issues."""
# Validate the path argument to prevent command injection
try:
src_path = validate_path("src/cleveragents")
except ValueError as e:
return False, f"Security: invalid path - {e}"
result = subprocess.run(
[
"bandit",
"-c",
"pyproject.toml",
"-r",
"src/cleveragents",
str(src_path),
"--severity-level",
"high",
"--format",
@@ -81,6 +152,7 @@ def check_security() -> tuple[bool, str]:
capture_output=True,
text=True,
check=False,
shell=False,
)
try:
data = json.loads(result.stdout)
@@ -96,19 +168,28 @@ def check_security() -> tuple[bool, str]:
def check_dead_code() -> tuple[bool, str]:
"""Check vulture finds no dead code."""
# Validate path arguments to prevent command injection
try:
src_path = validate_path("src/cleveragents")
whitelist_path = validate_path("vulture_whitelist.py")
exclude_path = validate_path("src/cleveragents/discovery")
except ValueError as e:
return False, f"Dead code: invalid path - {e}"
result = subprocess.run(
[
"vulture",
"src/cleveragents",
"vulture_whitelist.py",
str(src_path),
str(whitelist_path),
"--min-confidence",
"80",
"--exclude",
"src/cleveragents/discovery",
str(exclude_path),
],
capture_output=True,
text=True,
check=False,
shell=False,
)
if result.returncode == 0:
return True, "Dead code: none detected"
@@ -118,11 +199,17 @@ def check_dead_code() -> tuple[bool, str]:
def check_complexity(max_grade: str = "E") -> tuple[bool, str]:
"""Check no functions exceed maximum complexity grade."""
# Validate path argument to prevent command injection
try:
src_path = validate_path("src/cleveragents")
except ValueError as e:
return False, f"Complexity: invalid path - {e}"
result = subprocess.run(
[
"radon",
"cc",
"src/cleveragents",
str(src_path),
"--min",
max_grade,
"--show-complexity",
@@ -131,6 +218,7 @@ def check_complexity(max_grade: str = "E") -> tuple[bool, str]:
capture_output=True,
text=True,
check=False,
shell=False,
)
try:
data = json.loads(result.stdout)