diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 4ad0579f..9591f605 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -1,4 +1,8 @@ +## 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. ## 2025-05-18 - [SSRF Bypass via DNS Resolution Failure] **Vulnerability:** The `is_private_ip` function in `app/utils/network.py` failed open (returned `False`) when a hostname could not be resolved (`socket.gaierror`). **Learning:** This fail-open pattern was originally added to allow external domains in tests, but in production, it created a severe SSRF risk. An attacker could bypass SSRF protections by providing a URL that fails to resolve during the security check but resolves later (DNS rebinding), or by exploiting internal routing behaviors via unresolvable addresses. -**Prevention:** Always fail securely in network authorization functions. If a domain cannot be resolved to verify its safety, the request must be blocked (`return True` / default-deny). Tests should mock DNS resolution correctly instead of compromising production security logic. \ No newline at end of file +**Prevention:** Always fail securely in network authorization functions. If a domain cannot be resolved to verify its safety, the request must be blocked (`return True` / default-deny). Tests should mock DNS resolution correctly instead of compromising production security logic. 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: