From d8a628cc43bcc59ab76c722b6266e12799d03bed Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 21:27:00 +0000 Subject: [PATCH] Fix path validation vulnerability in SafeStaticFiles. Updated SafeStaticFiles to correctly block access to all hidden files and directories, securely handle null inputs, and fail closed by catching unexpected exceptions. Co-authored-by: socialawy <24765060+socialawy@users.noreply.github.com> --- .jules/sentinel.md | 5 +++++ src/audioformation/server/app.py | 21 +++++++++++++++------ uv.lock | 4 ++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index c1caaeb..34de243 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -6,3 +6,8 @@ **Vulnerability:** The custom `SafeStaticFiles` middleware in `src/audioformation/server/app.py` intended to block access to sensitive directories (like `00_CONFIG` and `.git`). However, it used `p = Path(path).lower()`, which raises an `AttributeError` because `pathlib.Path` objects lack a `.lower()` method. This effectively broke static file serving entirely (causing 500 errors) and represented a malformed security check. If such errors were ever 'swallowed' without raising an HTTP exception, it could result in 'failing open' and allowing access to sensitive files. **Learning:** Security checks that rely on path manipulation or normalization must be carefully tested for runtime exceptions. An unhandled exception in a security gate can either block legitimate traffic (Denial of Service) or, if caught improperly elsewhere, fail open. Always normalize the string representation of paths before converting them to `Path` objects. **Prevention:** Thoroughly test security middleware endpoints for both valid and invalid access attempts. Ensure that path string normalizations like `.lower()` are applied directly to the string before instantiating `Path(str(path).lower())`. + +## 2024-05-24 - [Fix path validation vulnerability in SafeStaticFiles] +**Vulnerability:** SafeStaticFiles didn't prevent access to hidden files and directories because it only checked if the file name started with ".env". It also did not handle None values or unexpected exceptions properly, violating fail-closed security principles. +**Learning:** Checking for specific hidden files like ".env" leaves the application vulnerable to other hidden files or directories being accessed. Furthermore, path validation logic must handle `None` values and unexpected exceptions correctly to fail closed and ensure security. +**Prevention:** In FastAPI apps and when serving static files, explicitly check `path is None` and wrap parsing logic in a `try...except` block, ensuring you raise an `HTTPException` on `Exception`. Check for any hidden file or directory access by verifying that no path parts start with a dot (`any(part.startswith(".") for part in p.parts)`). diff --git a/src/audioformation/server/app.py b/src/audioformation/server/app.py index 54243ef..11e0faa 100644 --- a/src/audioformation/server/app.py +++ b/src/audioformation/server/app.py @@ -20,15 +20,24 @@ class SafeStaticFiles(StaticFiles): - 00_CONFIG directory (contains secrets/API keys) - .env files - .git directories + - Any hidden files or directories """ async def get_response(self, path: str, scope) -> Response: - # Normalize path for check - p = Path(str(path).lower()) - if "00_config" in p.parts or p.name.startswith(".env") or ".git" in p.parts: - raise HTTPException( - status_code=403, detail="Access denied to sensitive resource" - ) + if path is None: + raise HTTPException(status_code=400, detail="Invalid path") + + try: + # Normalize path for check + p = Path(str(path).lower()) + if "00_config" in p.parts or ".git" in p.parts or any(part.startswith(".") for part in p.parts): + raise HTTPException( + status_code=403, detail="Access denied to sensitive resource" + ) + except HTTPException: + raise + except Exception: + raise HTTPException(status_code=400, detail="Invalid path") return await super().get_response(path, scope) diff --git a/uv.lock b/uv.lock index e34eac4..7970295 100644 --- a/uv.lock +++ b/uv.lock @@ -270,7 +270,7 @@ requires-dist = [ { name = "jsonschema", specifier = ">=4.21,<5" }, { name = "midiutil", marker = "extra == 'midi'", specifier = ">=1.2,<2" }, { name = "mutagen", marker = "extra == 'm4b'", specifier = ">=1.47,<2" }, - { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0,<2" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0,<3" }, { name = "numpy", specifier = ">=1.26,<3" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.6,<5" }, { name = "pydub", specifier = ">=0.25,<1" }, @@ -280,7 +280,7 @@ requires-dist = [ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0,<8" }, { name = "python-dotenv", marker = "extra == 'cloud'", specifier = ">=1.0,<2" }, { name = "python-multipart", marker = "extra == 'server'", specifier = ">=0.0.27,<1" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.9" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.1" }, { name = "silero-vad", marker = "extra == 'vad'", specifier = ">=6.0,<7" }, { name = "soundfile", specifier = ">=0.12,<1" }, { name = "transformers", specifier = ">=4.44,<6" },