sanitize user-supplied path components (#23990)
CI / AMD64 Build (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s

sanitize_filename leaves ".." intact and collapses variants like "..:" and "..*" to "..", so filesystem paths built from face names, classification model/category names, image ids, and trigger data could escape their base directory. Route every such site through new frigate/util/path.py helpers (safe_join, sanitize_path_component, sanitize_contained_path), which reject traversal and verify containment.

Worst case was DELETE /classification/{name}, which rmtree'd /media/frigate and /config while returning 200.

Important to note that all affected endpoints already require admin permission, so this sould be considered hardening rather than fixing exploitable code.
This commit is contained in:
Josh Hawkins
2026-08-13 21:59:46 -05:00
committed by GitHub
parent 812e5308a3
commit 11f8786459
8 changed files with 604 additions and 117 deletions
+6 -11
View File
@@ -13,7 +13,7 @@ from pathlib import Path
import psutil
from fastapi import APIRouter, Depends, Query, Request
from fastapi.responses import JSONResponse, StreamingResponse
from pathvalidate import sanitize_filename, sanitize_filepath
from pathvalidate import sanitize_filename
from peewee import DoesNotExist
from playhouse.shortcuts import model_to_dict
@@ -72,6 +72,7 @@ from frigate.record.export import (
PlaybackSourceEnum,
validate_ffmpeg_args,
)
from frigate.util.path import sanitize_contained_path
from frigate.util.time import is_current_hour
logger = logging.getLogger(__name__)
@@ -129,18 +130,12 @@ def _validate_export_case(export_case_id: str | None) -> JSONResponse | None:
def _sanitize_existing_image(
image_path: str | None,
) -> tuple[str | None, JSONResponse | None]:
# sanitize_filepath normalizes "\" to "/" but leaves ".." intact, so a path
# like "clips\..\..\etc/passwd" passes the CLIPS_DIR prefix check yet still
# escapes the directory once resolved. A valid snapshot path never uses "..".
if image_path and ".." in image_path:
return None, JSONResponse(
content={"success": False, "message": "Invalid image path"},
status_code=400,
)
if not image_path:
return None, None
existing_image = sanitize_filepath(image_path) if image_path else None
existing_image = sanitize_contained_path(image_path, CLIPS_DIR)
if existing_image and not existing_image.startswith(CLIPS_DIR):
if existing_image is None:
return None, JSONResponse(
content={"success": False, "message": "Invalid image path"},
status_code=400,