fix(#121): Remediate security issues from ORCH-121 review
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Priority Fixes (Required Before Production): H3: Add rate limiting to webhook endpoint - Added slowapi library for FastAPI rate limiting - Implemented per-IP rate limiting (100 req/min) on webhook endpoint - Added global rate limiting support via slowapi M4: Add subprocess timeouts to all gates - Added timeout=300 (5 minutes) to all subprocess.run() calls in gates - Implemented proper TimeoutExpired exception handling - Removed dead CalledProcessError handlers (check=False makes them unreachable) M2: Add input validation on QualityCheckRequest - Validate files array size (max 1000 files) - Validate file paths (no path traversal, no null bytes, no absolute paths) - Validate diff summary size (max 10KB) - Validate taskId and agentId format (non-empty) Additional Fixes: H1: Fix coverage.json path resolution - Use absolute paths resolved from project root - Validate path is within project boundaries (prevent path traversal) Code Review Cleanup: - Moved imports to module level in quality_orchestrator.py - Refactored mock detection logic into separate helper methods - Removed dead subprocess.CalledProcessError exception handlers from all gates Testing: - Added comprehensive tests for all security fixes - All 339 coordinator tests pass - All 447 orchestrator tests pass - Followed TDD principles (RED-GREEN-REFACTOR) Security Impact: - Prevents webhook DoS attacks via rate limiting - Prevents hung processes via subprocess timeouts - Prevents path traversal attacks via input validation - Prevents malformed input attacks via comprehensive validation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ class BuildGate:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False, # Don't raise on non-zero exit
|
||||
timeout=300, # 5 minute timeout
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
@@ -54,11 +55,11 @@ class BuildGate:
|
||||
details={"error": str(e)},
|
||||
)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
except subprocess.TimeoutExpired as e:
|
||||
return GateResult(
|
||||
passed=False,
|
||||
message="Build gate failed: Error running mypy",
|
||||
details={"error": str(e), "return_code": e.returncode},
|
||||
message=f"Build gate failed: mypy timed out after {e.timeout} seconds",
|
||||
details={"error": str(e), "timeout": e.timeout},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""CoverageGate - Enforces 85% minimum test coverage via pytest-cov."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
@@ -35,6 +36,7 @@ class CoverageGate:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False, # Don't raise on non-zero exit
|
||||
timeout=300, # 5 minute timeout
|
||||
)
|
||||
|
||||
# Try to read coverage data from coverage.json
|
||||
@@ -94,11 +96,11 @@ class CoverageGate:
|
||||
details={"error": str(e)},
|
||||
)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
except subprocess.TimeoutExpired as e:
|
||||
return GateResult(
|
||||
passed=False,
|
||||
message="Coverage gate failed: Error running pytest",
|
||||
details={"error": str(e), "return_code": e.returncode},
|
||||
message=f"Coverage gate failed: pytest timed out after {e.timeout} seconds",
|
||||
details={"error": str(e), "timeout": e.timeout},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -111,18 +113,28 @@ class CoverageGate:
|
||||
def _extract_coverage_from_json(self) -> float | None:
|
||||
"""Extract coverage percentage from coverage.json file.
|
||||
|
||||
Uses absolute path resolved from current working directory and validates
|
||||
that the path is within project boundaries to prevent path traversal attacks.
|
||||
|
||||
Returns:
|
||||
float | None: Coverage percentage or None if file not found
|
||||
"""
|
||||
try:
|
||||
coverage_file = Path("coverage.json")
|
||||
# Get absolute path from current working directory
|
||||
cwd = Path.cwd().resolve()
|
||||
coverage_file = (cwd / "coverage.json").resolve()
|
||||
|
||||
# Validate that coverage file is within project directory (prevent path traversal)
|
||||
if not str(coverage_file).startswith(str(cwd)):
|
||||
return None
|
||||
|
||||
if coverage_file.exists():
|
||||
with open(coverage_file) as f:
|
||||
data = json.load(f)
|
||||
percent = data.get("totals", {}).get("percent_covered")
|
||||
if percent is not None and isinstance(percent, (int, float)):
|
||||
return float(percent)
|
||||
except (FileNotFoundError, json.JSONDecodeError, KeyError):
|
||||
except (FileNotFoundError, json.JSONDecodeError, KeyError, OSError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ class LintGate:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False, # Don't raise on non-zero exit
|
||||
timeout=300, # 5 minute timeout
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
@@ -54,11 +55,11 @@ class LintGate:
|
||||
details={"error": str(e)},
|
||||
)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
except subprocess.TimeoutExpired as e:
|
||||
return GateResult(
|
||||
passed=False,
|
||||
message="Lint gate failed: Error running ruff",
|
||||
details={"error": str(e), "return_code": e.returncode},
|
||||
message=f"Lint gate failed: ruff timed out after {e.timeout} seconds",
|
||||
details={"error": str(e), "timeout": e.timeout},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -24,6 +24,7 @@ class TestGate:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False, # Don't raise on non-zero exit
|
||||
timeout=300, # 5 minute timeout
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
@@ -54,11 +55,11 @@ class TestGate:
|
||||
details={"error": str(e)},
|
||||
)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
except subprocess.TimeoutExpired as e:
|
||||
return GateResult(
|
||||
passed=False,
|
||||
message="Test gate failed: Error running pytest",
|
||||
details={"error": str(e), "return_code": e.returncode},
|
||||
message=f"Test gate failed: pytest timed out after {e.timeout} seconds",
|
||||
details={"error": str(e), "timeout": e.timeout},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user