Ensure that arbitrary reads / writes can't be executed from ffmpeg (#22607)

This commit is contained in:
Nicolas Mowen 2026-03-24 09:36:08 -06:00 committed by GitHub
parent de593c8e3f
commit 334245bd3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 67 additions and 0 deletions

View File

@ -46,6 +46,7 @@ from frigate.record.export import (
DEFAULT_TIME_LAPSE_FFMPEG_ARGS, DEFAULT_TIME_LAPSE_FFMPEG_ARGS,
PlaybackSourceEnum, PlaybackSourceEnum,
RecordingExporter, RecordingExporter,
validate_ffmpeg_args,
) )
from frigate.util.time import is_current_hour from frigate.util.time import is_current_hour
@ -547,6 +548,24 @@ def export_recording_custom(
export_id = f"{camera_name}_{''.join(random.choices(string.ascii_lowercase + string.digits, k=6))}" 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,
)
# Set default values if not provided (timelapse defaults) # Set default values if not provided (timelapse defaults)
if ffmpeg_input_args is None: if ffmpeg_input_args is None:
ffmpeg_input_args = "" ffmpeg_input_args = ""

View File

@ -36,6 +36,54 @@ logger = logging.getLogger(__name__)
DEFAULT_TIME_LAPSE_FFMPEG_ARGS = "-vf setpts=0.04*PTS -r 30" DEFAULT_TIME_LAPSE_FFMPEG_ARGS = "-vf setpts=0.04*PTS -r 30"
TIMELAPSE_DATA_INPUT_ARGS = "-an -skip_frame nokey" TIMELAPSE_DATA_INPUT_ARGS = "-an -skip_frame nokey"
# 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",
}
)
def validate_ffmpeg_args(args: str) -> tuple[bool, str]:
"""Validate that user-provided ffmpeg args don't allow input/output injection.
Blocks:
- The -i flag and other flags that read/write arbitrary files
- Absolute/relative file paths (potential extra outputs)
- URLs and ffmpeg protocol references (data exfiltration)
"""
if not args or not args.strip():
return True, ""
tokens = args.split()
for token in tokens:
# Block flags that could inject inputs or write to arbitrary files
if token.lower() in BLOCKED_FFMPEG_ARGS:
return False, f"Forbidden ffmpeg argument: {token}"
# Block tokens that look like file paths (potential output injection)
if (
token.startswith("/")
or token.startswith("./")
or token.startswith("../")
or token.startswith("~")
):
return False, "File paths are not allowed in custom ffmpeg arguments"
# Block URLs and ffmpeg protocol references (e.g. http://, tcp://, pipe:, file:)
if "://" in token or token.startswith("pipe:") or token.startswith("file:"):
return (
False,
"Protocol references are not allowed in custom ffmpeg arguments",
)
return True, ""
def lower_priority(): def lower_priority():
os.nice(PROCESS_PRIORITY_LOW) os.nice(PROCESS_PRIORITY_LOW)