From 89fe55249a1da3ddb6e78c41031b44104831139c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 18:54:50 +0900 Subject: [PATCH 1/4] fix: reject traversal in audio separator paths --- .../separation/audio_separator.py | 14 ++++++ .../analysis-engine/tests/test_separation.py | 49 +++++++++++++++++++ 2 files changed, 63 insertions(+) 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..72c3d989 100644 --- a/services/analysis-engine/src/bandscope_analysis/separation/audio_separator.py +++ b/services/analysis-engine/src/bandscope_analysis/separation/audio_separator.py @@ -41,6 +41,16 @@ def _read_model_profile_bytes(profile_path: Path) -> bytes: return profile_bytes +def _contains_parent_path_segment(path: Path) -> bool: + """Return true when a raw path contains a parent traversal segment.""" + path_text = str(path) + return any( + part == ".." + for separator in {os.sep, "/", "\\"} + for part in path_text.split(separator) + ) + + @dataclass(frozen=True) class AudioSeparationConfig: """Resource and band-split settings for local stem separation.""" @@ -133,6 +143,8 @@ 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() + if _contains_parent_path_segment(candidate): + raise ValueError("Path traversal attempt detected in selected audio path") try: path = candidate.resolve(strict=True) except FileNotFoundError as error: @@ -216,6 +228,8 @@ def _load_model_profile(self) -> dict[str, float]: if self.config.model_profile_path: profile_candidate = Path(self.config.model_profile_path).expanduser() + if _contains_parent_path_segment(profile_candidate): + raise ValueError("Path traversal attempt detected in selected 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..5f34837c 100644 --- a/services/analysis-engine/tests/test_separation.py +++ b/services/analysis-engine/tests/test_separation.py @@ -215,6 +215,22 @@ def test_audio_stem_separator_rejects_missing_audio_file(tmp_path) -> None: separator.separate(tmp_path / "missing.wav") +def test_audio_stem_separator_rejects_parent_traversal_in_audio_file(tmp_path) -> None: + """Ensure parent path segments are rejected before source path resolution.""" + separator = AudioStemSeparator(AudioSeparationConfig(target_sample_rate=8_000)) + + with pytest.raises(ValueError, match="Path traversal attempt detected"): + separator.separate(tmp_path / "nested" / ".." / "rehearsal.wav") + + +def test_audio_stem_separator_rejects_altsep_parent_traversal_in_audio_file() -> None: + """Ensure backslash traversal is rejected on non-Windows hosts.""" + separator = AudioStemSeparator(AudioSeparationConfig(target_sample_rate=8_000)) + + with pytest.raises(ValueError, match="Path traversal attempt detected"): + separator.separate("safe\\..\\rehearsal.wav") + + def test_audio_stem_separator_rejects_directory_source(tmp_path) -> None: """Ensure directories are not accepted as audio files.""" source_dir = tmp_path / "source-dir" @@ -277,6 +293,15 @@ def fail_decode(*args, **kwargs): assert str(tmp_path) not in str(error.value) +def test_audio_stem_separator_fit_length_zero() -> None: + """Ensure zero-length targets stay bounded and return an empty stem.""" + separator = AudioStemSeparator(AudioSeparationConfig(target_sample_rate=8_000)) + + fitted = separator._fit_length(np.ones(4, dtype=np.float32), 0) + + assert fitted.shape == (0,) + + def test_audio_stem_separator_uses_verified_local_model_profile(tmp_path) -> None: """Ensure local model profile overrides are applied only when checksum is verified.""" profile_path = tmp_path / "profile.json" @@ -403,6 +428,30 @@ def test_audio_stem_separator_rejects_missing_local_model_profile(tmp_path) -> N ) +def test_audio_stem_separator_rejects_parent_traversal_in_model_profile(tmp_path) -> None: + """Ensure parent path segments are rejected before profile path resolution.""" + with pytest.raises(ValueError, match="Path traversal attempt detected"): + AudioStemSeparator( + AudioSeparationConfig( + target_sample_rate=8_000, + model_profile_path=str(tmp_path / "profiles" / ".." / "profile.json"), + model_profile_sha256="0" * 64, + ) + ) + + +def test_audio_stem_separator_rejects_altsep_parent_traversal_in_model_profile() -> None: + """Ensure backslash traversal in profile paths is rejected on non-Windows hosts.""" + with pytest.raises(ValueError, match="Path traversal attempt detected"): + AudioStemSeparator( + AudioSeparationConfig( + target_sample_rate=8_000, + model_profile_path="profiles\\..\\profile.json", + model_profile_sha256="0" * 64, + ) + ) + + def test_audio_stem_separator_rejects_non_numeric_band_profile(tmp_path) -> None: """Ensure profile numeric coercion failures use bounded verification errors.""" profile_path = tmp_path / "profile.json" From f80aea3a98ef9e66276fb28b5c0d78532c233d40 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 20:12:36 +0900 Subject: [PATCH 2/4] fix: reject mixed separator traversal paths --- .../separation/audio_separator.py | 12 +++---- .../analysis-engine/tests/test_separation.py | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) 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 72c3d989..41375a17 100644 --- a/services/analysis-engine/src/bandscope_analysis/separation/audio_separator.py +++ b/services/analysis-engine/src/bandscope_analysis/separation/audio_separator.py @@ -42,13 +42,13 @@ def _read_model_profile_bytes(profile_path: Path) -> bytes: def _contains_parent_path_segment(path: Path) -> bool: - """Return true when a raw path contains a parent traversal segment.""" + """Return True when a raw path contains a parent traversal segment.""" path_text = str(path) - return any( - part == ".." - for separator in {os.sep, "/", "\\"} - for part in path_text.split(separator) - ) + normalized_path_text = path_text + for separator in {os.sep, os.altsep, "\\"}: + if separator and separator != "/": + normalized_path_text = normalized_path_text.replace(separator, "/") + return any(part == ".." for part in normalized_path_text.split("/")) @dataclass(frozen=True) diff --git a/services/analysis-engine/tests/test_separation.py b/services/analysis-engine/tests/test_separation.py index 5f34837c..f9aa96b4 100644 --- a/services/analysis-engine/tests/test_separation.py +++ b/services/analysis-engine/tests/test_separation.py @@ -231,6 +231,20 @@ def test_audio_stem_separator_rejects_altsep_parent_traversal_in_audio_file() -> separator.separate("safe\\..\\rehearsal.wav") +@pytest.mark.parametrize( + "audio_path", + ["safe/..\\rehearsal.wav", "safe\\../rehearsal.wav"], +) +def test_audio_stem_separator_rejects_mixed_separator_parent_traversal( + audio_path: str, +) -> None: + """Ensure mixed-separator traversal is rejected before path resolution.""" + separator = AudioStemSeparator(AudioSeparationConfig(target_sample_rate=8_000)) + + with pytest.raises(ValueError, match="Path traversal attempt detected"): + separator.separate(audio_path) + + def test_audio_stem_separator_rejects_directory_source(tmp_path) -> None: """Ensure directories are not accepted as audio files.""" source_dir = tmp_path / "source-dir" @@ -452,6 +466,24 @@ def test_audio_stem_separator_rejects_altsep_parent_traversal_in_model_profile() ) +@pytest.mark.parametrize( + "model_profile_path", + ["profiles/..\\profile.json", "profiles\\../profile.json"], +) +def test_audio_stem_separator_rejects_mixed_separator_parent_traversal_in_model_profile( + model_profile_path: str, +) -> None: + """Ensure mixed-separator traversal is rejected in model profile paths.""" + with pytest.raises(ValueError, match="Path traversal attempt detected"): + AudioStemSeparator( + AudioSeparationConfig( + target_sample_rate=8_000, + model_profile_path=model_profile_path, + model_profile_sha256="0" * 64, + ) + ) + + def test_audio_stem_separator_rejects_non_numeric_band_profile(tmp_path) -> None: """Ensure profile numeric coercion failures use bounded verification errors.""" profile_path = tmp_path / "profile.json" From d972ba59dfa0dafd0713050eee23846c5aab4387 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 15:20:17 +0900 Subject: [PATCH 3/4] fix: update anyhow for RustSec 2026-0190 --- apps/desktop/src-tauri/Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 4d9ae737..0df254ea 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "atk" From 56119789d9439293eae55bf7f5949a44cffbccbf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 19:45:56 +0900 Subject: [PATCH 4/4] fix: document quick-xml advisory exceptions --- apps/desktop/src-tauri/.cargo/audit.toml | 2 ++ apps/desktop/src-tauri/osv-scanner.toml | 8 ++++++++ docs/security/dependency-policy.md | 1 + 3 files changed, 11 insertions(+) diff --git a/apps/desktop/src-tauri/.cargo/audit.toml b/apps/desktop/src-tauri/.cargo/audit.toml index 9fc2a4f3..861e0aa5 100644 --- a/apps/desktop/src-tauri/.cargo/audit.toml +++ b/apps/desktop/src-tauri/.cargo/audit.toml @@ -17,4 +17,6 @@ ignore = [ "RUSTSEC-2025-0100", # unic-ucd-ident: unmaintained "RUSTSEC-2025-0098", # unic-ucd-version: unmaintained "RUSTSEC-2024-0429", # glib 0.18.5: VariantStrIter unsoundness, transitive via Tauri/wry/webkit2gtk/gtk GTK3 stack; remove when upstream drops or patches the chain + "RUSTSEC-2026-0194", # quick-xml 0.39.4: inherited via Tauri/plist and rfd/wayland-scanner; no compatible upstream release has moved both chains to quick-xml >=0.41.0 yet + "RUSTSEC-2026-0195", # quick-xml 0.39.4: same owner chain and removal condition as RUSTSEC-2026-0194 ] diff --git a/apps/desktop/src-tauri/osv-scanner.toml b/apps/desktop/src-tauri/osv-scanner.toml index 16b3b20e..c8fc5e44 100644 --- a/apps/desktop/src-tauri/osv-scanner.toml +++ b/apps/desktop/src-tauri/osv-scanner.toml @@ -65,3 +65,11 @@ reason = "Inherited through the current Tauri GTK3 owner chain and already track [[IgnoredVulns]] id = "RUSTSEC-2024-0429" reason = "glib 0.18.5 VariantStrIter advisory inherited through Tauri/wry/webkit2gtk/gtk; allowed only until upstream drops or patches the chain, with scope guarded by scripts/checks/verify_supply_chain.py." + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0194" +reason = "quick-xml 0.39.4 duplicate-attribute advisory is inherited through Tauri/plist and rfd/wayland-scanner; current compatible upstream crates do not yet allow quick-xml >=0.41.0, and this app does not expose those XML parser paths to untrusted user XML." + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0195" +reason = "quick-xml 0.39.4 namespace-allocation advisory is inherited through the same Tauri/plist and rfd/wayland-scanner owner chain as RUSTSEC-2026-0194; remove once compatible upstream crates move to quick-xml >=0.41.0." diff --git a/docs/security/dependency-policy.md b/docs/security/dependency-policy.md index d3a9680e..d7c7acad 100644 --- a/docs/security/dependency-policy.md +++ b/docs/security/dependency-policy.md @@ -104,6 +104,7 @@ Current controlled exceptions: - No Python vulnerability exceptions are active. `GHSA-5239-wwwm-4pmq` (`Pygments <2.20.0`) was removed by locking `Pygments` to `2.20.0`; the CI `security-audit` workflow must run `pip-audit --local --strict` against the synced `uv` environment without a targeted ignore for that advisory. - Cargo audit warnings for legacy `gtk3` vulnerabilities (e.g. `RUSTSEC-2024-0413`) inherited through Tauri v2 `wry`/`webkit2gtk` integration are explicitly allowed. These are deep framework dependencies with no alternative, so they are documented exceptions and ignored by default. - `RUSTSEC-2024-0429` for `glib 0.18.5` is allowed only for the `VariantStrIter` advisory inherited through the Tauri/wry/webkit2gtk/gtk GTK3 stack. A compatible lockfile refresh can move the desktop stack to `tauri 2.11.3`, `wry 0.55.1`, `tao 0.35.3`, `muda 0.19.3`, and related transitive patches, but it still does not move this stack to patched `glib >=0.20.0`; the exception must remain encoded in repo-controlled audit configuration and guarded by `scripts/checks/verify_supply_chain.py`, and it must be removed when upstream drops or patches the chain. +- `RUSTSEC-2026-0194` and `RUSTSEC-2026-0195` for `quick-xml 0.39.4` are allowed only while the current compatible upstream owner chains still require vulnerable `quick-xml`: `plist 1.9.0` through Tauri, and `wayland-scanner 0.31.10` through Linux `rfd`/Wayland dependencies. `quick-xml >=0.41.0` is patched, but `plist 1.9.0` requires `quick-xml ^0.39.2` and the current `wayland-scanner` release also has no compatible patched path. BandScope does not expose either owner chain as a user-controlled XML ingestion surface; the exception must stay encoded in repo-controlled cargo-audit and OSV configuration, and must be removed once compatible upstream crates publish a patched dependency path. Retired third-party deprecation and advisory signal: