فهرست منبع

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
maziggy 3 ماه پیش
والد
کامیت
4d94286e53
2فایلهای تغییر یافته به همراه16 افزوده شده و 2 حذف شده
  1. 8 2
      backend/app/api/routes/archives.py
  2. 8 0
      backend/app/api/routes/projects.py

+ 8 - 2
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")
             raise HTTPException(400, "Audio must be .mp3, .wav, .m4a, .aac, or .ogg")
 
 
         audio_content = await audio.read()
         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 = Path(tempfile.gettempdir()) / f"audio_{archive_id}{suffix}"
         audio_temp_path.write_bytes(audio_content)
         audio_temp_path.write_bytes(audio_content)
 
 
@@ -1605,8 +1608,11 @@ async def process_timelapse(
         else:
         else:
             # Save as new file alongside original
             # Save as new file alongside original
             filename = output_filename or f"{archive.print_name or 'timelapse'}_edited.mp4"
             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 "._- ")
             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"):
             if not filename.endswith(".mp4"):
                 filename += ".mp4"
                 filename += ".mp4"
             output_path = archive_dir / filename
             output_path = archive_dir / filename

+ 8 - 0
backend/app/api/routes/projects.py

@@ -907,6 +907,10 @@ async def download_attachment(
     _: User | None = RequirePermissionIfAuthEnabled(Permission.PROJECTS_READ),
     _: User | None = RequirePermissionIfAuthEnabled(Permission.PROJECTS_READ),
 ):
 ):
     """Download an attachment from a project."""
     """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
     # Verify project exists
     result = await db.execute(select(Project).where(Project.id == project_id))
     result = await db.execute(select(Project).where(Project.id == project_id))
     project = result.scalar_one_or_none()
     project = result.scalar_one_or_none()
@@ -939,6 +943,10 @@ async def delete_attachment(
     _: User | None = RequirePermissionIfAuthEnabled(Permission.PROJECTS_UPDATE),
     _: User | None = RequirePermissionIfAuthEnabled(Permission.PROJECTS_UPDATE),
 ):
 ):
     """Delete an attachment from a project."""
     """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
     # Verify project exists
     result = await db.execute(select(Project).where(Project.id == project_id))
     result = await db.execute(select(Project).where(Project.id == project_id))
     project = result.scalar_one_or_none()
     project = result.scalar_one_or_none()