From 1018ea17d9a9c60b37ce3afacc9055300050bf99 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:53:18 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20path=20traversal=20vulnerability=20in=20file=20utiliti?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: CRITICAL 💡 Vulnerability: The generic file hashing utility `app/utils/file_operations.py:hash_file` was vulnerable to path traversal. An attacker controlling the `filepath` argument could read arbitrary files on the system by passing relative paths like `../../../etc/passwd` or providing absolute paths directly. 🎯 Impact: This could lead to Arbitrary File Read and potential information disclosure. 🔧 Fix: Used `pathlib.Path.resolve()` to resolve both the target file path and the allowed base directory (`settings.workdir`). Added a strict check to ensure the resolved target path is strictly within the allowed boundary using `filepath_obj.relative_to(workdir_obj)`, catching the `ValueError` raised when the path is out of bounds. This safely blocks both relative traversal attacks and arbitrary absolute paths, without breaking legitimate relative application paths. ✅ Verification: Ran the test suite `pytest tests/test_path_traversal_security.py -v` successfully, which explicitly checks for `FileNotFoundError` upon traversal attempts. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- .jules/sentinel.md | 8 ++++---- app/utils/file_operations.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 7833fd12..c0b4089f 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -1,4 +1,4 @@ -## 2024-05-24 - SSRF in WebDAV connection test -**Vulnerability:** The `_test_webdav_connection` function had a custom SSRF check that failed to resolve DNS names, allowing attackers to bypass the check by providing a domain that resolves to an internal IP (e.g., `127.0.0.1`). -**Learning:** DNS resolution is required for robust SSRF protection when validating URLs provided by users. -**Prevention:** Use a centralized `is_private_ip` function (now in `app/utils/network.py`) that resolves the hostname to its IPs and checks if any are private. +## 2026-03-20 - Safe Path Traversal Prevention in Low-Level Utilities +**Vulnerability:** The generic file utility `hash_file` in `app/utils/file_operations.py` accepted any file path and was vulnerable to reading arbitrary files via path traversal (e.g., `../../../etc/passwd`) or absolute paths if an attacker could control the `filepath` argument. +**Learning:** Naively checking for `".." in path` breaks legitimate relative paths used internally by the application. Blocking absolute paths entirely also breaks functionality. Input validation should occur at the API boundary, but for defense-in-depth, low-level utilities must enforce expected boundaries (e.g., the application's `workdir`). +**Prevention:** Use `pathlib.Path.resolve()` on both the target path and the allowed base directory (`settings.workdir`). Ensure the resolved target path is strictly within the allowed boundary using `filepath_obj.relative_to(workdir_obj)`, catching the `ValueError` that is raised when the path is out of bounds. This safely blocks both relative traversal attacks and arbitrary absolute paths. diff --git a/app/utils/file_operations.py b/app/utils/file_operations.py index 485da36d..03e6fd0f 100644 --- a/app/utils/file_operations.py +++ b/app/utils/file_operations.py @@ -7,8 +7,19 @@ def hash_file(filepath: str | Path, chunk_size: int = 65536) -> str: Returns the SHA-256 hash of the file at 'filepath'. Reads the file in chunks to handle large files efficiently. """ + from app.config import settings + + filepath_obj = Path(filepath).resolve() + workdir_obj = Path(settings.workdir).resolve() + + # Security check: Ensure the resolved path is strictly within the allowed workdir + try: + filepath_obj.relative_to(workdir_obj) + except ValueError: + raise FileNotFoundError(f"Access denied: path traversal attempt or file outside workdir '{filepath}'") + sha256 = hashlib.sha256() - with open(filepath, "rb") as f: + with open(filepath_obj, "rb") as f: while True: data = f.read(chunk_size) if not data: