Resolve go2rtc stream placeholders the same way everywhere

The go2rtc startup script expanded stream sources with str.format(),
which also supports attribute and index access, while the config API
check used substitute_frigate_vars and fell back to the raw string when a
placeholder could not be resolved. A placeholder the API check could not
resolve could therefore expand into a restricted source at startup.

The startup script now uses substitute_frigate_vars, like the rest of the
config, and the API check rejects a stream source whose placeholders
cannot be resolved. The startup script's own copy of the FRIGATE_* env
var map is removed; the shared one also honors CREDENTIALS_DIRECTORY.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Blake Blackshear
2026-10-14 08:45:52 -05:00
co-authored by Claude Opus 5.5
parent 21f5ab5a1e
commit ac0f39268c
3 changed files with 30 additions and 15 deletions
@@ -3,7 +3,6 @@
import json import json
import os import os
import sys import sys
from pathlib import Path
from typing import Any from typing import Any
from ruamel.yaml import YAML from ruamel.yaml import YAML
@@ -25,15 +24,6 @@ sys.path.remove("/opt/frigate")
yaml = YAML() yaml = YAML()
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
# read docker secret files as env vars too
if os.path.isdir("/run/secrets"):
for secret_file in os.listdir("/run/secrets"):
if secret_file.startswith("FRIGATE_"):
FRIGATE_ENV_VARS[secret_file] = (
Path(os.path.join("/run/secrets", secret_file)).read_text().strip()
)
config_file = find_config_file() config_file = find_config_file()
try: try:
@@ -113,7 +103,7 @@ for name in list(go2rtc_config.get("streams", {})):
if isinstance(stream, str): if isinstance(stream, str):
try: try:
formatted_stream = stream.format(**FRIGATE_ENV_VARS) formatted_stream = substitute_frigate_vars(stream)
if is_restricted_go2rtc_source(formatted_stream): if is_restricted_go2rtc_source(formatted_stream):
print( print(
f"[ERROR] Stream '{name}' uses a restricted source (echo/expr/exec) which is disabled by default for security. " f"[ERROR] Stream '{name}' uses a restricted source (echo/expr/exec) which is disabled by default for security. "
@@ -122,7 +112,7 @@ for name in list(go2rtc_config.get("streams", {})):
del go2rtc_config["streams"][name] del go2rtc_config["streams"][name]
continue continue
go2rtc_config["streams"][name] = formatted_stream go2rtc_config["streams"][name] = formatted_stream
except KeyError as e: except (KeyError, ValueError) as e:
print( print(
"[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info." "[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info."
) )
@@ -132,7 +122,7 @@ for name in list(go2rtc_config.get("streams", {})):
filtered_streams = [] filtered_streams = []
for i, stream_item in enumerate(stream): for i, stream_item in enumerate(stream):
try: try:
formatted_stream = stream_item.format(**FRIGATE_ENV_VARS) formatted_stream = substitute_frigate_vars(stream_item)
if is_restricted_go2rtc_source(formatted_stream): if is_restricted_go2rtc_source(formatted_stream):
print( print(
f"[ERROR] Stream '{name}' item {i + 1} uses a restricted source (echo/expr/exec) which is disabled by default for security. " f"[ERROR] Stream '{name}' item {i + 1} uses a restricted source (echo/expr/exec) which is disabled by default for security. "
@@ -141,7 +131,7 @@ for name in list(go2rtc_config.get("streams", {})):
continue continue
filtered_streams.append(formatted_stream) filtered_streams.append(formatted_stream)
except KeyError as e: except (KeyError, ValueError) as e:
print( print(
"[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info." "[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info."
) )
@@ -7,6 +7,7 @@ from unittest.mock import patch
import ruamel.yaml import ruamel.yaml
from frigate.config.env import FRIGATE_ENV_VARS
from frigate.models import Event, Recordings, ReviewSegment from frigate.models import Event, Recordings, ReviewSegment
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
@@ -167,3 +168,24 @@ class TestConfigGo2rtcStreamRestrictions(BaseTestHttp):
assert resp.status_code == 400 assert resp.status_code == 400
assert "Restricted go2rtc stream source" in resp.json()["message"] assert "Restricted go2rtc stream source" in resp.json()["message"]
assert self._read_config() == before assert self._read_config() == before
# --- {FRIGATE_*} placeholders resolve the same way as at go2rtc startup ---
def test_set_rejects_unresolvable_placeholder(self):
before = self._read_config()
with patch.dict(FRIGATE_ENV_VARS, {"FRIGATE_RTSP_USER": "user"}):
for src in (
"{FRIGATE_RTSP_USER!r}fmpeg:front_door#raw=-vn",
"{FRIGATE_RTSP_USER:>5}",
"rtsp://{FRIGATE_UNDEFINED}@10.0.0.2:554/video",
):
resp = self._set_streams({"new_stream": [src]})
assert resp.status_code == 400, src
assert self._read_config() == before
def test_set_allows_resolvable_placeholder(self):
src = "rtsp://{FRIGATE_RTSP_USER}@10.0.0.2:554/video"
with patch.dict(FRIGATE_ENV_VARS, {"FRIGATE_RTSP_USER": "user"}):
resp = self._set_streams({"new_stream": [src]})
assert resp.status_code == 200, resp.text
assert src in self._read_config()
+4 -1
View File
@@ -1073,10 +1073,13 @@ def find_restricted_go2rtc_stream_changes(
restricted.append(name) restricted.append(name)
break break
# go2rtc startup resolves sources with the same substitution, and
# refuses to start on one it cannot resolve
try: try:
resolved_source = substitute_frigate_vars(source) resolved_source = substitute_frigate_vars(source)
except (KeyError, ValueError): except (KeyError, ValueError):
resolved_source = source restricted.append(name)
break
if is_restricted_go2rtc_api_source( if is_restricted_go2rtc_api_source(
source source