From 4d94286e53fb209995d73bbf7f8bbcda51f0d8a5 Mon Sep 17 00:00:00 2001 From: maziggy Date: Thu, 5 Feb 2026 18:09:42 +0100 Subject: [PATCH] Fix CodeQL path injection vulnerabilities - projects.py: Add path traversal validation to attachment endpoints - Reject filenames containing /, \, or .. - Prevents directory traversal attacks via URL parameters - archives.py: Strengthen timelapse processing input validation - Validate audio suffix against whitelist (not just filename check) - Reject output filenames with .., empty, or dot-prefixed names - Fall back to safe default filename if validation fails --- backend/app/api/routes/archives.py | 10 ++++++++-- backend/app/api/routes/projects.py | 8 ++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/app/api/routes/archives.py b/backend/app/api/routes/archives.py index 83dd13ed7..0faf9883e 100644 --- a/backend/app/api/routes/archives.py +++ b/backend/app/api/routes/archives.py @@ -1590,7 +1590,10 @@ async def process_timelapse( raise HTTPException(400, "Audio must be .mp3, .wav, .m4a, .aac, or .ogg") audio_content = await audio.read() - suffix = Path(audio.filename).suffix + # Extract and validate suffix to prevent path injection + suffix = Path(audio.filename).suffix.lower() + if suffix not in (".mp3", ".wav", ".m4a", ".aac", ".ogg"): + raise HTTPException(400, "Invalid audio file extension") audio_temp_path = Path(tempfile.gettempdir()) / f"audio_{archive_id}{suffix}" audio_temp_path.write_bytes(audio_content) @@ -1605,8 +1608,11 @@ async def process_timelapse( else: # Save as new file alongside original filename = output_filename or f"{archive.print_name or 'timelapse'}_edited.mp4" - # Sanitize filename + # Sanitize filename - remove path separators and traversal sequences filename = "".join(c for c in filename if c.isalnum() or c in "._- ") + # Prevent path traversal + if ".." in filename or not filename or filename.startswith("."): + filename = f"timelapse_{archive_id}_edited" if not filename.endswith(".mp4"): filename += ".mp4" output_path = archive_dir / filename diff --git a/backend/app/api/routes/projects.py b/backend/app/api/routes/projects.py index 9528f96a7..82a5289f9 100644 --- a/backend/app/api/routes/projects.py +++ b/backend/app/api/routes/projects.py @@ -907,6 +907,10 @@ async def download_attachment( _: User | None = RequirePermissionIfAuthEnabled(Permission.PROJECTS_READ), ): """Download an attachment from a project.""" + # Validate filename to prevent path traversal + if "/" in filename or "\\" in filename or ".." in filename or not filename: + raise HTTPException(status_code=400, detail="Invalid filename") + # Verify project exists result = await db.execute(select(Project).where(Project.id == project_id)) project = result.scalar_one_or_none() @@ -939,6 +943,10 @@ async def delete_attachment( _: User | None = RequirePermissionIfAuthEnabled(Permission.PROJECTS_UPDATE), ): """Delete an attachment from a project.""" + # Validate filename to prevent path traversal + if "/" in filename or "\\" in filename or ".." in filename or not filename: + raise HTTPException(status_code=400, detail="Invalid filename") + # Verify project exists result = await db.execute(select(Project).where(Project.id == project_id)) project = result.scalar_one_or_none()