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: