diff --git a/frigate/api/export.py b/frigate/api/export.py index 71aedfb11..056a0613f 100644 --- a/frigate/api/export.py +++ b/frigate/api/export.py @@ -548,23 +548,27 @@ def export_recording_custom( export_id = f"{camera_name}_{''.join(random.choices(string.ascii_lowercase + string.digits, k=6))}" - # Validate user-provided ffmpeg args to prevent injection - for args_label, args_value in [ - ("input", ffmpeg_input_args), - ("output", ffmpeg_output_args), - ]: - if args_value is not None: - valid, message = validate_ffmpeg_args(args_value) - if not valid: - return JSONResponse( - content=( - { - "success": False, - "message": f"Invalid ffmpeg {args_label} arguments: {message}", - } - ), - status_code=400, - ) + # Validate user-provided ffmpeg args to prevent injection. + # Admin users are trusted and skip validation. + is_admin = request.headers.get("remote-role", "") == "admin" + + if not is_admin: + for args_label, args_value in [ + ("input", ffmpeg_input_args), + ("output", ffmpeg_output_args), + ]: + if args_value is not None: + valid, message = validate_ffmpeg_args(args_value) + if not valid: + return JSONResponse( + content=( + { + "success": False, + "message": f"Invalid ffmpeg {args_label} arguments: {message}", + } + ), + status_code=400, + ) # Set default values if not provided (timelapse defaults) if ffmpeg_input_args is None: diff --git a/frigate/record/export.py b/frigate/record/export.py index ddd14dfd6..173a55624 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -36,22 +36,20 @@ 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. -# filter flags are blocked because source filters like movie= and -# amovie= can read arbitrary files from the filesystem. +# ffmpeg flags that can read from or write to arbitrary files BLOCKED_FFMPEG_ARGS = frozenset( { "-i", "-filter_script", - "-vstats_file", - "-passlogfile", - "-sdp_file", - "-dump_attachment", "-filter_complex", "-lavfi", "-vf", "-af", "-filter", + "-vstats_file", + "-passlogfile", + "-sdp_file", + "-dump_attachment", "-attach", } ) @@ -62,8 +60,11 @@ def validate_ffmpeg_args(args: str) -> tuple[bool, str]: Blocks: - The -i flag and other flags that read/write arbitrary files + - Filter flags (can read files via movie=/amovie= source filters) - Absolute/relative file paths (potential extra outputs) - URLs and ffmpeg protocol references (data exfiltration) + + Admin users skip this validation entirely since they are trusted. """ if not args or not args.strip(): return True, "" diff --git a/frigate/video/ffmpeg.py b/frigate/video/ffmpeg.py index 852ea4a16..d30dc3b18 100644 --- a/frigate/video/ffmpeg.py +++ b/frigate/video/ffmpeg.py @@ -471,8 +471,16 @@ class CameraWatchdog(threading.Thread): p["cmd"], self.logger, p["logpipe"], ffmpeg_process=p["process"] ) - # Update stall metrics based on last processed frame timestamp + # Prune expired reconnect timestamps now = datetime.now().timestamp() + while ( + self.reconnect_timestamps and self.reconnect_timestamps[0] < now - 3600 + ): + self.reconnect_timestamps.popleft() + if self.reconnects: + self.reconnects.value = len(self.reconnect_timestamps) + + # Update stall metrics based on last processed frame timestamp processed_ts = ( float(self.detection_frame.value) if self.detection_frame else 0.0 )