From 2256f54288747ff4d2a2a8be00bbe9424e087d90 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 3 Jul 2026 03:56:39 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL/H?= =?UTF-8?q?IGH]=20Fix=20path=20traversal=20in=20backend=20API=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added cross-platform traversal check for local source paths. * Blocked payload execution by rejecting paths evaluating strictly sequentially using backslashes for path traversal evasion attempts. * Evaluated against `.jules/sentinel.md` learnings. * Re-generated passing API tests confirming correct evasion failure handling behavior. --- .jules/sentinel.md | 5 ++ .../src/bandscope_analysis/api.py | 8 ++++ services/analysis-engine/tests/test_api.py | 47 +++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index c7a67127..f4e0ad92 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. + +## 2024-05-20 - Python Path Traversal Mitigation bypass +**Vulnerability:** Path traversal detection in Python backend APIs relied solely on checking the input path string or basic parsed parts which might not adequately catch sequences like `..` when intermixed with different path separators. +**Learning:** Using `if ".." in Path(path).parts` incorrectly assumes `Path` parses mixed cross-platform separators reliably for untrusted input, particularly in heterogeneous environments. Instead, explicitly reject directory traversal sequences by replacing `\` with `/` and splitting on `/` to prevent bypassing automated CI vulnerability scanners (like Strix) as explicitly requested in memory. +**Prevention:** Always manually split the target path using both `/` and `\` manually instead of relying solely on `os.sep`, `os.altsep`, or `pathlib.Path.parts`. This ensures cross-platform payloads are reliably blocked even when executed on POSIX runners. diff --git a/services/analysis-engine/src/bandscope_analysis/api.py b/services/analysis-engine/src/bandscope_analysis/api.py index a193bce2..c29aa4b8 100644 --- a/services/analysis-engine/src/bandscope_analysis/api.py +++ b/services/analysis-engine/src/bandscope_analysis/api.py @@ -285,6 +285,10 @@ def validate_analysis_job_request(payload: object) -> AnalysisJobRequest: file_size_bytes = local_source.get("fileSizeBytes") if not isinstance(source_path, str) or not source_path.strip(): raise ValueError("Invalid analysis job request: invalid field 'localSource.sourcePath'") + if ".." in source_path.replace("\\", "/").split("/"): + raise ValueError( + "Invalid analysis job request: path traversal detected in 'localSource.sourcePath'" + ) if not isinstance(file_name, str) or not file_name.strip(): raise ValueError("Invalid analysis job request: invalid field 'localSource.fileName'") if extension not in {"wav", "mp3", "flac", "m4a"}: @@ -307,10 +311,14 @@ def validate_analysis_job_request(payload: object) -> AnalysisJobRequest: if cache_root is not None: if not isinstance(cache_root, str) or not cache_root.strip(): raise ValueError("Invalid analysis job request: invalid field 'cacheRoot'") + if ".." in cache_root.replace("\\", "/").split("/"): + raise ValueError("Invalid analysis job request: path traversal detected in 'cacheRoot'") normalized["cacheRoot"] = cache_root if temp_root is not None: if not isinstance(temp_root, str) or not temp_root.strip(): raise ValueError("Invalid analysis job request: invalid field 'tempRoot'") + if ".." in temp_root.replace("\\", "/").split("/"): + raise ValueError("Invalid analysis job request: path traversal detected in 'tempRoot'") normalized["tempRoot"] = temp_root return normalized diff --git a/services/analysis-engine/tests/test_api.py b/services/analysis-engine/tests/test_api.py index ea55cba2..b92e6ae1 100644 --- a/services/analysis-engine/tests/test_api.py +++ b/services/analysis-engine/tests/test_api.py @@ -261,6 +261,53 @@ def test_validate_analysis_job_request_rejects_bad_payloads() -> None: }, "tempRoot", ), + ( + { + "sourceKind": "local_audio", + "projectId": "project-1", + "sourceLabel": "Late Night Set", + "roleFocus": [], + "localSource": { + "sourcePath": "/Users/test/Music/late-night-set.wav", + "fileName": "late-night-set.wav", + "extension": "wav", + "fileSizeBytes": 1024000, + }, + "cacheRoot": "/tmp/../secret", + }, + "path traversal", + ), + ( + { + "sourceKind": "local_audio", + "projectId": "project-1", + "sourceLabel": "Late Night Set", + "roleFocus": [], + "localSource": { + "sourcePath": "/Users/test/Music/late-night-set.wav", + "fileName": "late-night-set.wav", + "extension": "wav", + "fileSizeBytes": 1024000, + }, + "tempRoot": "C:\\temp\\..\\secret", + }, + "path traversal", + ), + ( + { + "sourceKind": "local_audio", + "projectId": "project-1", + "sourceLabel": "Late Night Set", + "roleFocus": [], + "localSource": { + "sourcePath": "../secret.wav", + "fileName": "late-night-set.wav", + "extension": "wav", + "fileSizeBytes": 1024000, + }, + }, + "path traversal", + ), ] for payload, message in cases: