diff --git a/frigate/record/export.py b/frigate/record/export.py index aac96fba3f..5da5e818ff 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -124,8 +124,8 @@ def _base_flag(token: str) -> str: def _validate_filtergraph(value: str) -> tuple[bool, str]: """Validate a filtergraph value, allowing only filters in _SAFE_FILTERS.""" - # No safe filter needs these; they only show up in read/exfil payloads. - if "://" in value or ".." in value: + # None of the safe filters need any of these + if any(token in value for token in ("://", "..", "[", "]")): return False, "Invalid filter graph in custom ffmpeg arguments" lowered = value.lower() @@ -136,9 +136,7 @@ def _validate_filtergraph(value: str) -> tuple[bool, str]: # filters never use unescaped "," or ";" in their arguments, so splitting on # them to recover filter names cannot hide a disallowed filter. for spec in re.split(r"[;,]", value): - # Strip leading/trailing [link] labels (e.g. "[in]scale=...[out]"). - spec = re.sub(r"^(\[[^\]]*\])+", "", spec.strip()) - spec = re.sub(r"(\[[^\]]*\])+$", "", spec).strip() + spec = spec.strip() if not spec: continue diff --git a/frigate/test/test_export.py b/frigate/test/test_export.py index 87b5a64f6a..7612a4144f 100644 --- a/frigate/test/test_export.py +++ b/frigate/test/test_export.py @@ -64,6 +64,12 @@ class TestValidateFfmpegArgs(unittest.TestCase): # marker embedded as an option of an otherwise-allowed filter name self.assertRejected("-vf scale=movie=/etc/passwd") + def test_filtergraph_brackets_rejected(self): + # link labels aren't needed for safe filters; rejecting "[" / "]" keeps + # filtergraph validation linear (no ReDoS on attacker input) + self.assertRejected("-vf [in]scale=640:480[out]") + self.assertRejected("-vf " + "[" * 5000) + def test_preset_file_read_rejected(self): # cwd-anchored traversal slipped past the old startswith() path check self.assertRejected("-fpre frigate/../../../etc/passwd")