diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 34122c2b4..ddba0a6ca 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -28,3 +28,8 @@ **Vulnerability:** The Rust backend (`apps/desktop/src-tauri/src/main.rs`) did not enforce a maximum URL length limit when processing YouTube URLs via `import_youtube_url`. While the frontend enforced `MAX_YOUTUBE_URL_LENGTH = 2000` via the input element, this could be bypassed by an attacker sending requests directly to the Tauri backend API, potentially causing a Denial of Service (DoS) due to unbounded URL parsing and regex matching. **Learning:** Input validation must occur at the entry point of untrusted data on the backend, even if it is also validated on the frontend. Relying solely on frontend validation for constraints like string length can expose the backend to resource exhaustion vulnerabilities. **Prevention:** Always enforce constraints like maximum length, format validation, and sanitization at the earliest possible point on the backend, typically at the API boundary, regardless of frontend safeguards. + +## 2026-09-24 - Log Forging Vulnerability in Python Logging +**Vulnerability:** Python f-strings in logging statements allow untrusted user input (e.g., file paths) to be evaluated immediately and included in the log format string. Attackers could inject newlines or other control characters to forge log entries or obscure traces. +**Learning:** Deferred string formatting (e.g., using `%s`) is preferred for logging because it delegates interpolation to the logging framework, which can safely handle types but doesn't sanitize control characters by itself. Wrapping untrusted input in `repr()` explicitly escapes newlines and control characters, neutralizing injection attempts. +**Prevention:** Always use deferred formatting (`%s`) combined with `repr()` (or an equivalent sanitization function) when logging any user-controlled input like paths, URLs, or query parameters. diff --git a/services/analysis-engine/src/bandscope_analysis/cli.py b/services/analysis-engine/src/bandscope_analysis/cli.py index 6838ee711..91ad3fa77 100644 --- a/services/analysis-engine/src/bandscope_analysis/cli.py +++ b/services/analysis-engine/src/bandscope_analysis/cli.py @@ -90,7 +90,7 @@ def main() -> int: try: temporal_analyzer = TemporalAnalyzer() features = temporal_analyzer.analyze(audio_path) - logging.info(f"Extracted BPM: {features['bpm']}") + logging.info("Extracted BPM: %s", features["bpm"]) except Exception: logging.warning( "Temporal analysis failed for %s; continuing with safe fallback.", diff --git a/services/analysis-engine/src/bandscope_analysis/temporal/analyzer.py b/services/analysis-engine/src/bandscope_analysis/temporal/analyzer.py index 7fe5ae6f7..4a590f57e 100644 --- a/services/analysis-engine/src/bandscope_analysis/temporal/analyzer.py +++ b/services/analysis-engine/src/bandscope_analysis/temporal/analyzer.py @@ -73,7 +73,7 @@ def analyze(self, audio_path: str | Path) -> TemporalFeatures: if not path.exists() or not path.is_file(): raise FileNotFoundError(f"Audio file not found: {path_str}") - logger.info(f"Loading and decoding audio: {path_str}") + logger.info("Loading and decoding audio: %s", repr(path_str)) try: with path.open("rb") as fileobj: @@ -128,7 +128,7 @@ def analyze(self, audio_path: str | Path) -> TemporalFeatures: bpm_val = float(tempo[0]) if isinstance(tempo, np.ndarray) else float(tempo) - logger.info(f"Analysis complete: {bpm_val:.1f} BPM, {len(beat_times)} beats detected.") + logger.info("Analysis complete: %.1f BPM, %d beats detected.", bpm_val, len(beat_times)) return { "bpm": bpm_val, @@ -140,5 +140,5 @@ def analyze(self, audio_path: str | Path) -> TemporalFeatures: } except Exception as e: - logger.error(f"Failed to analyze audio {path_str}: {e}") + logger.error("Failed to analyze audio %s: %s", repr(path_str), e) raise ValueError(f"Temporal analysis failed: {e}") from e diff --git a/services/analysis-engine/tests/test_supply_chain_policy.py b/services/analysis-engine/tests/test_supply_chain_policy.py index 1d8224c5a..6a0853944 100644 --- a/services/analysis-engine/tests/test_supply_chain_policy.py +++ b/services/analysis-engine/tests/test_supply_chain_policy.py @@ -1275,9 +1275,7 @@ def test_workflow_concurrency_cancels_only_superseded_pr_heads() -> None: workflow = (workflows_dir / workflow_name).read_text(encoding="utf-8") assert "concurrency:" in workflow, workflow_name assert "cancel-in-progress: false" in workflow, workflow_name - assert "contents: read" in workflow or "permissions: read-all" in workflow, ( - workflow_name - ) + assert "contents: read" in workflow or "permissions: read-all" in workflow, workflow_name assert "pull_request:" not in (workflows_dir / "release.yml").read_text(encoding="utf-8")