mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-10 00:32:48 +03:00
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
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:
+41
-37
@@ -16,7 +16,6 @@ import numpy as np
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.params import Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from pathvalidate import sanitize_filename
|
||||
from peewee import JOIN, DoesNotExist, fn, operator
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
@@ -56,11 +55,12 @@ from frigate.api.defs.response.generic_response import GenericResponse
|
||||
from frigate.api.defs.tags import Tags
|
||||
from frigate.comms.event_metadata_updater import EventMetadataTypeEnum
|
||||
from frigate.config.classification import ObjectClassificationType
|
||||
from frigate.const import CLIPS_DIR, TRIGGER_DIR
|
||||
from frigate.const import CLIPS_DIR
|
||||
from frigate.embeddings import EmbeddingsContext
|
||||
from frigate.models import Event, ReviewSegment, Timeline, Trigger
|
||||
from frigate.track.object_processing import TrackedObject
|
||||
from frigate.util.file import get_event_thumbnail_bytes, load_event_snapshot_image
|
||||
from frigate.util.path import get_trigger_thumbnail_path, safe_join
|
||||
from frigate.util.time import get_dst_transitions, get_tz_modifiers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -1452,10 +1452,10 @@ async def set_attributes(
|
||||
continue
|
||||
|
||||
# Get available labels from dataset directory
|
||||
dataset_dir = os.path.join(CLIPS_DIR, sanitize_filename(model_key), "dataset")
|
||||
dataset_dir = safe_join(CLIPS_DIR, model_key, "dataset")
|
||||
available_labels = set()
|
||||
|
||||
if os.path.exists(dataset_dir):
|
||||
if dataset_dir and os.path.exists(dataset_dir):
|
||||
for category_name in os.listdir(dataset_dir):
|
||||
category_dir = os.path.join(dataset_dir, category_name)
|
||||
if os.path.isdir(category_dir):
|
||||
@@ -1959,18 +1959,13 @@ def create_trigger_embedding(
|
||||
if body.type == "thumbnail":
|
||||
# Save image to the triggers directory
|
||||
try:
|
||||
os.makedirs(
|
||||
os.path.join(TRIGGER_DIR, sanitize_filename(camera_name)),
|
||||
exist_ok=True,
|
||||
)
|
||||
with open(
|
||||
os.path.join(
|
||||
TRIGGER_DIR,
|
||||
sanitize_filename(camera_name),
|
||||
f"{sanitize_filename(body.data)}.webp",
|
||||
),
|
||||
"wb",
|
||||
) as f:
|
||||
webp_path = get_trigger_thumbnail_path(camera_name, body.data)
|
||||
|
||||
if webp_path is None:
|
||||
raise ValueError(f"Invalid trigger thumbnail path for {body.data}")
|
||||
|
||||
os.makedirs(os.path.dirname(webp_path), exist_ok=True)
|
||||
with open(webp_path, "wb") as f:
|
||||
f.write(thumbnail)
|
||||
logger.debug(
|
||||
f"Writing thumbnail for trigger with data {body.data} in {camera_name}."
|
||||
@@ -2042,10 +2037,16 @@ def update_trigger_embedding(
|
||||
if body.type == "description":
|
||||
embedding = context.generate_description_embedding(body.data)
|
||||
elif body.type == "thumbnail":
|
||||
webp_file = sanitize_filename(body.data) + ".webp"
|
||||
webp_path = os.path.join(
|
||||
TRIGGER_DIR, sanitize_filename(camera_name), webp_file
|
||||
)
|
||||
webp_path = get_trigger_thumbnail_path(camera_name, body.data)
|
||||
|
||||
if webp_path is None:
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": False,
|
||||
"message": f"Invalid data for {body.type} trigger",
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
try:
|
||||
event: Event = Event.get(Event.id == body.data)
|
||||
@@ -2102,13 +2103,14 @@ def update_trigger_embedding(
|
||||
# Update existing trigger
|
||||
if trigger.data != body.data: # Delete old thumbnail only if data changes
|
||||
try:
|
||||
os.remove(
|
||||
os.path.join(
|
||||
TRIGGER_DIR,
|
||||
sanitize_filename(camera_name),
|
||||
f"{trigger.data}.webp",
|
||||
old_path = get_trigger_thumbnail_path(camera_name, trigger.data)
|
||||
|
||||
if old_path is None:
|
||||
raise ValueError(
|
||||
f"Invalid trigger thumbnail path for {trigger.data}"
|
||||
)
|
||||
)
|
||||
|
||||
os.remove(old_path)
|
||||
logger.debug(
|
||||
f"Deleted thumbnail for trigger with data {trigger.data} in {camera_name}."
|
||||
)
|
||||
@@ -2142,12 +2144,13 @@ def update_trigger_embedding(
|
||||
if body.type == "thumbnail":
|
||||
# Save image to the triggers directory
|
||||
try:
|
||||
camera_path = os.path.join(TRIGGER_DIR, sanitize_filename(camera_name))
|
||||
os.makedirs(camera_path, exist_ok=True)
|
||||
with open(
|
||||
os.path.join(camera_path, f"{sanitize_filename(body.data)}.webp"),
|
||||
"wb",
|
||||
) as f:
|
||||
thumbnail_path = get_trigger_thumbnail_path(camera_name, body.data)
|
||||
|
||||
if thumbnail_path is None:
|
||||
raise ValueError(f"Invalid trigger thumbnail path for {body.data}")
|
||||
|
||||
os.makedirs(os.path.dirname(thumbnail_path), exist_ok=True)
|
||||
with open(thumbnail_path, "wb") as f:
|
||||
f.write(thumbnail)
|
||||
logger.debug(
|
||||
f"Writing thumbnail for trigger with data {body.data} in {camera_name}."
|
||||
@@ -2218,11 +2221,12 @@ def delete_trigger_embedding(
|
||||
)
|
||||
|
||||
try:
|
||||
os.remove(
|
||||
os.path.join(
|
||||
TRIGGER_DIR, sanitize_filename(camera_name), f"{trigger.data}.webp"
|
||||
)
|
||||
)
|
||||
thumbnail_path = get_trigger_thumbnail_path(camera_name, trigger.data)
|
||||
|
||||
if thumbnail_path is None:
|
||||
raise ValueError(f"Invalid trigger thumbnail path for {trigger.data}")
|
||||
|
||||
os.remove(thumbnail_path)
|
||||
logger.debug(
|
||||
f"Deleted thumbnail for trigger with data {trigger.data} in {camera_name}."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user