From b821420deed5f03d531f2b0cbdd44c03039af8db Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:45:04 -0500 Subject: [PATCH] Miscellaneous improvements (#22714) * scrub genai API keys and onvif credentials from config endpoint * enforce camera access in thumbnail tracked-object fallback The /events/{id}/thumbnail endpoint called require_camera_access when loading persisted events but skipped the check in the tracked-object fallback path for in-progress events. A restricted viewer could retrieve thumbnails from cameras they should not have access to. * block filter and attach flags in custom ffmpeg export args The ffmpeg argument blocklist missed -filter_complex, -lavfi, -vf, -af, -filter, and -attach. These flags can read arbitrary files via source filters like movie= and amovie=, bypassing the existing -i block. A user with camera access could exploit this through the custom export endpoint. * enforce camera access on VLM monitor endpoint POST /vlm/monitor allowed any authenticated user to start VLM monitoring on any camera without checking camera access. A viewer restricted to specific cameras could monitor cameras they should not have access to. * enforce camera access in chat start_camera_watch tool The start_camera_watch tool called via POST /chat/completion did not validate camera access, allowing a restricted viewer to start VLM monitoring on cameras outside their allowed set through the chat interface. * restrict review summary endpoint to admin role * fix require_role call passing string instead of list * fix section config uiSchema merge replacing base entries mergeSectionConfig was replacing the entire base uiSchema when a level override (global/camera) also defined one, causing base-level ui:after/ui:before directives to be silently dropped. This broke the SemanticSearchReindex button which was defined in base uiSchema. --- frigate/api/app.py | 11 +++++++++++ frigate/api/chat.py | 5 +++++ frigate/api/media.py | 3 ++- frigate/api/review.py | 2 +- frigate/record/export.py | 10 +++++++++- web/src/utils/configUtil.ts | 7 +++++-- 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/frigate/api/app.py b/frigate/api/app.py index 0126e5d03..f3cc472a8 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -142,9 +142,20 @@ def config(request: Request): # remove the proxy secret config["proxy"].pop("auth_secret", None) + # remove genai api keys + for genai_name, genai_cfg in config.get("genai", {}).items(): + if isinstance(genai_cfg, dict): + genai_cfg.pop("api_key", None) + for camera_name, camera in request.app.frigate_config.cameras.items(): camera_dict = config["cameras"][camera_name] + # remove onvif credentials + onvif_dict = camera_dict.get("onvif", {}) + if onvif_dict: + onvif_dict.pop("user", None) + onvif_dict.pop("password", None) + # clean paths for input in camera_dict.get("ffmpeg", {}).get("inputs", []): input["path"] = clean_camera_user_pass(input["path"]) diff --git a/frigate/api/chat.py b/frigate/api/chat.py index 136c425f2..82b2c92eb 100644 --- a/frigate/api/chat.py +++ b/frigate/api/chat.py @@ -15,6 +15,7 @@ from pydantic import BaseModel from frigate.api.auth import ( allow_any_authenticated, get_allowed_cameras_for_filter, + require_camera_access, ) from frigate.api.defs.query.events_query_parameters import EventsQueryParams from frigate.api.defs.request.chat_body import ChatCompletionRequest @@ -672,6 +673,8 @@ async def _execute_start_camera_watch( if camera not in config.cameras: return {"error": f"Camera '{camera}' not found."} + await require_camera_access(camera, request=request) + genai_manager = request.app.genai_manager vision_client = genai_manager.vision_client or genai_manager.tool_client if vision_client is None: @@ -1156,6 +1159,8 @@ async def start_vlm_monitor( status_code=404, ) + await require_camera_access(body.camera, request=request) + vision_client = genai_manager.vision_client or genai_manager.tool_client if vision_client is None: return JSONResponse( diff --git a/frigate/api/media.py b/frigate/api/media.py index fd48a11e1..489c008b4 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -896,6 +896,7 @@ async def event_thumbnail( if event_id in camera_state.tracked_objects: tracked_obj = camera_state.tracked_objects.get(event_id) if tracked_obj is not None: + await require_camera_access(camera_state.name, request=request) thumbnail_bytes = tracked_obj.get_thumbnail(extension.value) except Exception: return JSONResponse( @@ -1066,7 +1067,7 @@ def grid_snapshot( @router.delete( - "/{camera_name}/region_grid", dependencies=[Depends(require_role("admin"))] + "/{camera_name}/region_grid", dependencies=[Depends(require_role(["admin"]))] ) def clear_region_grid(request: Request, camera_name: str): """Clear the region grid for a camera.""" diff --git a/frigate/api/review.py b/frigate/api/review.py index d2e8063d5..06480dd8c 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -742,7 +742,7 @@ async def set_not_reviewed( @router.post( "/review/summarize/start/{start_ts}/end/{end_ts}", - dependencies=[Depends(allow_any_authenticated())], + dependencies=[Depends(require_role(["admin"]))], description="Use GenAI to summarize review items over a period of time.", ) def generate_review_summary(request: Request, start_ts: float, end_ts: float): diff --git a/frigate/record/export.py b/frigate/record/export.py index 1c02fd112..ddd14dfd6 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -36,7 +36,9 @@ logger = logging.getLogger(__name__) DEFAULT_TIME_LAPSE_FFMPEG_ARGS = "-vf setpts=0.04*PTS -r 30" TIMELAPSE_DATA_INPUT_ARGS = "-an -skip_frame nokey" -# ffmpeg flags that can read from or write to arbitrary files +# ffmpeg flags that can read from or write to arbitrary files. +# filter flags are blocked because source filters like movie= and +# amovie= can read arbitrary files from the filesystem. BLOCKED_FFMPEG_ARGS = frozenset( { "-i", @@ -45,6 +47,12 @@ BLOCKED_FFMPEG_ARGS = frozenset( "-passlogfile", "-sdp_file", "-dump_attachment", + "-filter_complex", + "-lavfi", + "-vf", + "-af", + "-filter", + "-attach", } ) diff --git a/web/src/utils/configUtil.ts b/web/src/utils/configUtil.ts index bbd73fdab..fb233a457 100644 --- a/web/src/utils/configUtil.ts +++ b/web/src/utils/configUtil.ts @@ -649,8 +649,11 @@ const mergeSectionConfig = ( return srcValue ?? objValue; } - if (key === "uiSchema" && srcValue !== undefined) { - return srcValue; + if (key === "uiSchema") { + if (objValue && srcValue) { + return merge({}, objValue, srcValue); + } + return srcValue ?? objValue; } return undefined;