From becb9374f38958d2ed1c705b5148adf40ff13e1c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:03:58 +0000 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20Path=20Traversal=20via=20Folder=20ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: CRITICAL 💡 Vulnerability: Path Traversal via Folder ID (PK) injection from API response. 🎯 Impact: An attacker controlling the API or MITM could inject path traversal payloads (e.g. ../../etc/passwd) into file operations. 🔧 Fix: Validated Folder IDs against a strict whitelist (^[a-zA-Z0-9_.-]+$). ✅ Verification: Added tests/test_folder_id_validation.py proving invalid IDs are rejected. Co-authored-by: abhimehro <84992105+abhimehro@users.noreply.github.com> --- .jules/sentinel.md | 8 ++++ main.py | 57 ++++++++++++++++++++------ pyproject.toml | 3 ++ tests/test_folder_id_validation.py | 65 ++++++++++++++++++++++++++++++ uv.lock | 3 ++ 5 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 tests/test_folder_id_validation.py diff --git a/.jules/sentinel.md b/.jules/sentinel.md index dd57a2a..b6ec121 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -45,3 +45,11 @@ **Vulnerability:** `sanitize_for_log` in `main.py` was stripping quotes from `repr()` output for aesthetic reasons, inadvertently exposing strings starting with `=`, `+`, `-`, `@` to formula injection if logs are viewed in Excel. **Learning:** Security controls (like `repr()`) can be weakened by subsequent "cleanup" steps. Always consider downstream consumption of logs (e.g., CSV export). **Prevention:** Check for dangerous prefixes before stripping quotes. If detected, retain the quotes to force string literal interpretation. + +## 2026-05-23 - Path Traversal via API Response (ID Validation) + +**Vulnerability:** The application trusted "PK" (ID) fields from the external API and used them directly in URL construction for deletion. An attacker compromising the API or MITM could return a malicious ID like `../../etc/passwd` to trigger path traversal or other injection attacks. + +**Learning:** Even trusted APIs should be treated as untrusted sources for critical identifiers used in system operations or path construction. + +**Prevention:** Whitelist valid characters for all identifiers (e.g. `^[a-zA-Z0-9_.-]+$`) and validate them immediately upon receipt, before any use. diff --git a/main.py b/main.py index 1f4f047..72e9f33 100644 --- a/main.py +++ b/main.py @@ -183,6 +183,9 @@ def check_env_permissions(env_path: str = ".env") -> None: # Pre-compiled regex patterns for hot-path validation (>2x speedup on 10k+ items) PROFILE_ID_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$") +# Folder IDs (PK) are typically alphanumeric but can contain other safe chars. +# We whitelist to prevent path traversal and injection. +FOLDER_ID_PATTERN = re.compile(r"^[a-zA-Z0-9_.-]+$") RULE_PATTERN = re.compile(r"^[a-zA-Z0-9.\-_:*/@]+$") # Parallel processing configuration @@ -737,6 +740,17 @@ def validate_profile_id(profile_id: str, log_errors: bool = True) -> bool: return True +def validate_folder_id(folder_id: str, log_errors: bool = True) -> bool: + """Validates folder ID (PK) format to prevent path traversal.""" + if not folder_id: + return False + if not FOLDER_ID_PATTERN.match(folder_id): + if log_errors: + log.error(f"Invalid folder ID format: {sanitize_for_log(folder_id)}") + return False + return True + + def is_valid_rule(rule: str) -> bool: """ Validates that a rule is safe to use. @@ -1105,11 +1119,14 @@ def list_existing_folders(client: httpx.Client, profile_id: str) -> Dict[str, st try: data = _api_get(client, f"{API_BASE}/{profile_id}/groups").json() folders = data.get("body", {}).get("groups", []) - return { - f["group"].strip(): f["PK"] - for f in folders - if f.get("group") and f.get("PK") - } + result = {} + for f in folders: + if not f.get("group") or not f.get("PK"): + continue + pk = str(f["PK"]) + if validate_folder_id(pk): + result[f["group"].strip()] = pk + return result except (httpx.HTTPError, KeyError) as e: log.error(f"Failed to list existing folders: {sanitize_for_log(e)}") return {} @@ -1173,7 +1190,12 @@ def verify_access_and_get_folders( # Skip entries with empty or None values for required fields if not name or not pk: continue - result[str(name).strip()] = str(pk) + + pk_str = str(pk) + if not validate_folder_id(pk_str): + continue + + result[str(name).strip()] = pk_str return result except (ValueError, TypeError, AttributeError) as err: @@ -1395,24 +1417,31 @@ def create_folder( # Check if it returned a single group object if isinstance(body, dict) and "group" in body and "PK" in body["group"]: - pk = body["group"]["PK"] + pk = str(body["group"]["PK"]) + if not validate_folder_id(pk): + log.error(f"API returned invalid folder ID: {sanitize_for_log(pk)}") + return None log.info( "Created folder %s (ID %s) [Direct]", sanitize_for_log(name), sanitize_for_log(pk), ) - return str(pk) + return pk # Check if it returned a list containing our group if isinstance(body, dict) and "groups" in body: for grp in body["groups"]: if grp.get("group") == name: + pk = str(grp["PK"]) + if not validate_folder_id(pk): + log.error(f"API returned invalid folder ID: {sanitize_for_log(pk)}") + continue log.info( "Created folder %s (ID %s) [Direct]", sanitize_for_log(name), - sanitize_for_log(grp["PK"]), + sanitize_for_log(pk), ) - return str(grp["PK"]) + return pk except Exception as e: log.debug( f"Could not extract ID from POST response: " f"{sanitize_for_log(e)}" @@ -1426,12 +1455,16 @@ def create_folder( for grp in groups: if grp["group"].strip() == name.strip(): + pk = str(grp["PK"]) + if not validate_folder_id(pk): + log.error(f"API returned invalid folder ID: {sanitize_for_log(pk)}") + return None log.info( "Created folder %s (ID %s) [Polled]", sanitize_for_log(name), - sanitize_for_log(grp["PK"]), + sanitize_for_log(pk), ) - return str(grp["PK"]) + return pk except Exception as e: log.warning( f"Error fetching groups on attempt {attempt}: {sanitize_for_log(e)}" diff --git a/pyproject.toml b/pyproject.toml index 4cc9410..8ed4121 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,3 +15,6 @@ dev = [ "pytest-mock>=3.10.0", "pytest-xdist>=3.0.0", ] + +[dependency-groups] +dev = [] diff --git a/tests/test_folder_id_validation.py b/tests/test_folder_id_validation.py new file mode 100644 index 0000000..ed08921 --- /dev/null +++ b/tests/test_folder_id_validation.py @@ -0,0 +1,65 @@ +import importlib +import sys +from unittest.mock import MagicMock, patch + +import httpx +import pytest + +import main + + +def reload_main_with_env(monkeypatch): + monkeypatch.delenv("NO_COLOR", raising=False) + with patch("sys.stderr") as mock_stderr, patch("sys.stdout") as mock_stdout: + mock_stderr.isatty.return_value = True + mock_stdout.isatty.return_value = True + + module = sys.modules.get("main") + if module is None: + module = importlib.import_module("main") + + importlib.reload(module) + return module + + +def test_verify_access_and_get_folders_filters_malicious_ids(monkeypatch): + """ + Verify that verify_access_and_get_folders filters out malicious Folder IDs + containing path traversal characters (../). + """ + m = reload_main_with_env(monkeypatch) + mock_client = MagicMock() + + # Malicious Folder ID with path traversal + malicious_id = "../../etc/passwd" + # Malicious Folder ID with dangerous characters + malicious_id_2 = "foo;rm -rf /" + + mock_response = MagicMock() + mock_response.json.return_value = { + "body": { + "groups": [ + {"group": "Safe Folder", "PK": "safe_id_123"}, + {"group": "Safe Folder 2", "PK": "safe-id-456_789"}, + {"group": "Malicious Folder", "PK": malicious_id}, + {"group": "Malicious Folder 2", "PK": malicious_id_2} + ] + } + } + mock_client.get.return_value = mock_response + mock_response.raise_for_status.return_value = None + + # Function should filter out malicious IDs + result = m.verify_access_and_get_folders(mock_client, "valid_profile") + + assert result is not None + + # Check that valid IDs are preserved + assert "Safe Folder" in result + assert result["Safe Folder"] == "safe_id_123" + assert "Safe Folder 2" in result + assert result["Safe Folder 2"] == "safe-id-456_789" + + # Check that malicious IDs are removed + assert "Malicious Folder" not in result + assert "Malicious Folder 2" not in result diff --git a/uv.lock b/uv.lock index 53e9b38..4b9c404 100644 --- a/uv.lock +++ b/uv.lock @@ -59,6 +59,9 @@ requires-dist = [ ] provides-extras = ["dev"] +[package.metadata.requires-dev] +dev = [] + [[package]] name = "execnet" version = "2.1.2" From 166617282de818999605f8806b0c04849800f9e2 Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Wed, 18 Feb 2026 13:04:14 -0600 Subject: [PATCH 2/7] Update main.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 72e9f33..de8c59e 100644 --- a/main.py +++ b/main.py @@ -744,7 +744,7 @@ def validate_folder_id(folder_id: str, log_errors: bool = True) -> bool: """Validates folder ID (PK) format to prevent path traversal.""" if not folder_id: return False - if not FOLDER_ID_PATTERN.match(folder_id): + if folder_id in (".", "..") or not FOLDER_ID_PATTERN.match(folder_id): if log_errors: log.error(f"Invalid folder ID format: {sanitize_for_log(folder_id)}") return False From e731ebe0e990029c0b32e1ad031590b9eb19e226 Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Wed, 18 Feb 2026 13:04:20 -0600 Subject: [PATCH 3/7] Update main.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index de8c59e..fb953d8 100644 --- a/main.py +++ b/main.py @@ -1418,7 +1418,7 @@ def create_folder( # Check if it returned a single group object if isinstance(body, dict) and "group" in body and "PK" in body["group"]: pk = str(body["group"]["PK"]) - if not validate_folder_id(pk): + if not validate_folder_id(pk, log_errors=False): log.error(f"API returned invalid folder ID: {sanitize_for_log(pk)}") return None log.info( From 0794fdfc014b9bba4d0447ef1b794116eeb1440b Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Wed, 18 Feb 2026 13:04:26 -0600 Subject: [PATCH 4/7] Update main.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index fb953d8..41677b1 100644 --- a/main.py +++ b/main.py @@ -1433,7 +1433,7 @@ def create_folder( for grp in body["groups"]: if grp.get("group") == name: pk = str(grp["PK"]) - if not validate_folder_id(pk): + if not validate_folder_id(pk, log_errors=False): log.error(f"API returned invalid folder ID: {sanitize_for_log(pk)}") continue log.info( From e4899c78be448045a9d31bb6686747b182f7c9a3 Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Wed, 18 Feb 2026 13:04:33 -0600 Subject: [PATCH 5/7] Update main.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 41677b1..ea75ce3 100644 --- a/main.py +++ b/main.py @@ -1456,7 +1456,7 @@ def create_folder( for grp in groups: if grp["group"].strip() == name.strip(): pk = str(grp["PK"]) - if not validate_folder_id(pk): + if not validate_folder_id(pk, log_errors=False): log.error(f"API returned invalid folder ID: {sanitize_for_log(pk)}") return None log.info( From 53bca532bda65860922ea7827fab5a470f3b076e Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Wed, 18 Feb 2026 13:05:18 -0600 Subject: [PATCH 6/7] Update pyproject.toml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8ed4121..4cc9410 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,3 @@ dev = [ "pytest-mock>=3.10.0", "pytest-xdist>=3.0.0", ] - -[dependency-groups] -dev = [] From 94119f477c37ee27151e123459706d528b9ad8ce Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Wed, 18 Feb 2026 13:05:24 -0600 Subject: [PATCH 7/7] Update test_folder_id_validation.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_folder_id_validation.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_folder_id_validation.py b/tests/test_folder_id_validation.py index ed08921..26a2f7d 100644 --- a/tests/test_folder_id_validation.py +++ b/tests/test_folder_id_validation.py @@ -2,12 +2,6 @@ import sys from unittest.mock import MagicMock, patch -import httpx -import pytest - -import main - - def reload_main_with_env(monkeypatch): monkeypatch.delenv("NO_COLOR", raising=False) with patch("sys.stderr") as mock_stderr, patch("sys.stdout") as mock_stdout: