diff --git a/.jules/sentinel.md b/.jules/sentinel.md index c7a67127..12f06f20 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,8 @@ **Vulnerability:** CSV formula injection mitigation was naive, missing leading whitespace, tabs, and newlines. **Learning:** Checking `/^[=+\-@]/` is not sufficient, as OWASP states that spaces and tabs before the formula triggers will also execute the formula in applications like Excel. **Prevention:** Use a regex that allows leading whitespace (e.g. `/^[\s\uFEFF\xA0]*[=+\-@\t\r\n]/`) and include standalone tabs or new lines which are also injection vectors. + +## 2025-02-24 - Path Traversal via os.path.expanduser +**Vulnerability:** Path traversal using `.expanduser()` on untrusted path input. +**Learning:** Avoid using `.expanduser()` on untrusted input paths in backend Python services, as it allows arbitrary path traversal. +**Prevention:** Instead, explicitly reject directory traversal sequences (e.g., checking for '..') and use standard path resolving methods like `Path(audio_path).resolve(strict=True)` to safely process local directories. Verify to pass automated CI vulnerability scanners (like Strix) and strictly maintain test cases. diff --git a/services/analysis-engine/src/bandscope_analysis/separation/audio_separator.py b/services/analysis-engine/src/bandscope_analysis/separation/audio_separator.py index cb65e391..8e680f69 100644 --- a/services/analysis-engine/src/bandscope_analysis/separation/audio_separator.py +++ b/services/analysis-engine/src/bandscope_analysis/separation/audio_separator.py @@ -132,7 +132,10 @@ def separate(self, audio_path: str | Path) -> AudioSeparationResult: def _resolve_audio_file(self, audio_path: str | Path) -> Path: """Normalize and validate the selected source path.""" - candidate = Path(audio_path).expanduser() + audio_path_str = str(audio_path) + if ".." in audio_path_str: + raise ValueError(f"Path traversal detected in audio file path: {audio_path_str}") + candidate = Path(audio_path) try: path = candidate.resolve(strict=True) except FileNotFoundError as error: @@ -215,7 +218,12 @@ def _load_model_profile(self) -> dict[str, float]: expected_sha256 = _BANDSPLIT_PROFILE_SHA256 if self.config.model_profile_path: - profile_candidate = Path(self.config.model_profile_path).expanduser() + profile_path_str = str(self.config.model_profile_path) + if ".." in profile_path_str: + raise ValueError( + f"Path traversal detected in model profile path: {profile_path_str}" + ) + profile_candidate = Path(self.config.model_profile_path) try: profile_path = profile_candidate.resolve(strict=True) except FileNotFoundError as error: diff --git a/services/analysis-engine/tests/test_separation.py b/services/analysis-engine/tests/test_separation.py index 81e27cc1..a6611bbf 100644 --- a/services/analysis-engine/tests/test_separation.py +++ b/services/analysis-engine/tests/test_separation.py @@ -207,6 +207,26 @@ def test_audio_stem_separator_assigns_boundary_frequency_to_drums_only() -> None assert vocal_peak < drum_peak * 0.001 +def test_audio_stem_separator_rejects_path_traversal_in_audio_path() -> None: + """Ensure path traversal attempts in the audio path are strictly rejected.""" + separator = AudioStemSeparator(AudioSeparationConfig(target_sample_rate=8_000)) + + with pytest.raises(ValueError, match="Path traversal detected in audio file path"): + separator.separate("../../../etc/passwd") + + +def test_audio_stem_separator_rejects_path_traversal_in_model_profile() -> None: + """Ensure path traversal attempts in the model profile path are strictly rejected.""" + config = AudioSeparationConfig( + target_sample_rate=8_000, + model_profile_path="../../../etc/passwd", + model_profile_sha256="fake_sha", + ) + + with pytest.raises(ValueError, match="Path traversal detected in model profile path"): + AudioStemSeparator(config) + + def test_audio_stem_separator_rejects_missing_audio_file(tmp_path) -> None: """Ensure missing local files fail before decode without leaking a full path.""" separator = AudioStemSeparator(AudioSeparationConfig(target_sample_rate=8_000))