From 9b5fc8fb0fcec2e5b295e1d9ac9b35b5ff9cfd79 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 15 May 2026 09:33:07 -0500 Subject: [PATCH] reject restricted go2rtc stream sources when added via api --- .../rootfs/usr/local/go2rtc/create_config.py | 6 +---- frigate/api/camera.py | 21 ++++++++++++--- .../test/http_api/test_http_camera_access.py | 27 +++++++++++++++++++ frigate/util/services.py | 5 ++++ 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index 71897be0a7..68d207ee52 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -18,6 +18,7 @@ from frigate.const import ( ) from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode from frigate.util.config import find_config_file +from frigate.util.services import is_restricted_source sys.path.remove("/opt/frigate") @@ -128,11 +129,6 @@ if LIBAVFORMAT_VERSION_MAJOR < 59: go2rtc_config["ffmpeg"]["rtsp"] = rtsp_args -def is_restricted_source(stream_source: str) -> bool: - """Check if a stream source is restricted (echo, expr, or exec).""" - return stream_source.strip().startswith(("echo:", "expr:", "exec:")) - - for name in list(go2rtc_config.get("streams", {})): stream = go2rtc_config["streams"][name] diff --git a/frigate/api/camera.py b/frigate/api/camera.py index 54d508cbd2..9b4bfccdfc 100644 --- a/frigate/api/camera.py +++ b/frigate/api/camera.py @@ -38,7 +38,7 @@ from frigate.util.builtin import clean_camera_user_pass from frigate.util.camera_cleanup import cleanup_camera_db, cleanup_camera_files from frigate.util.config import find_config_file from frigate.util.image import run_ffmpeg_snapshot -from frigate.util.services import ffprobe_stream +from frigate.util.services import ffprobe_stream, is_restricted_source logger = logging.getLogger(__name__) @@ -147,9 +147,24 @@ def go2rtc_add_stream(request: Request, stream_name: str, src: str = ""): params = {"name": stream_name} if src: try: - params["src"] = substitute_frigate_vars(src) + resolved_src = substitute_frigate_vars(src) except KeyError: - params["src"] = src + resolved_src = src + + if is_restricted_source(resolved_src): + logger.warning( + "Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)", + stream_name, + ) + return JSONResponse( + content={ + "success": False, + "message": "Restricted stream source type", + }, + status_code=400, + ) + + params["src"] = resolved_src r = requests.put( "http://127.0.0.1:1984/api/streams", diff --git a/frigate/test/http_api/test_http_camera_access.py b/frigate/test/http_api/test_http_camera_access.py index 211c84bb4f..09310daf54 100644 --- a/frigate/test/http_api/test_http_camera_access.py +++ b/frigate/test/http_api/test_http_camera_access.py @@ -357,6 +357,33 @@ class TestGo2rtcStreamAccess(BaseTestHttp): f"got {resp.status_code}" ) + def test_add_stream_rejects_restricted_source(self): + """PUT /go2rtc/streams must reject exec:/echo:/expr: sources even for + admins""" + app = self._make_app(_MULTI_CAMERA_CONFIG) + with AuthTestClient(app) as client: + for src in ( + "exec:/tmp/rev.sh", + "echo:foo", + "expr:bar", + " exec:/tmp/rev.sh", + ): + resp = client.put(f"/go2rtc/streams/revshell?src={src}") + assert resp.status_code == 400, ( + f"Expected 400 for restricted src {src!r}; got {resp.status_code}" + ) + assert resp.json().get("success") is False + + def test_add_stream_allows_non_restricted_source(self): + """A normal stream URL should pass the restricted-source check and reach + the (unavailable in tests) go2rtc proxy — so we expect 500, not 400.""" + app = self._make_app(_MULTI_CAMERA_CONFIG) + with AuthTestClient(app) as client: + resp = client.put("/go2rtc/streams/legit?src=rtsp://10.0.0.1:554/video") + assert resp.status_code != 400, ( + f"Non-restricted source should not be rejected with 400; got {resp.status_code}" + ) + def test_stream_alias_blocked_when_owning_camera_disallowed(self): """limited_user cannot access a stream alias that belongs to a camera they are not allowed to see.""" diff --git a/frigate/util/services.py b/frigate/util/services.py index 5ee15f8b48..4e8cc44031 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -778,6 +778,11 @@ def get_hailo_temps() -> dict[str, float]: return temps +def is_restricted_source(stream_source: str) -> bool: + """Check if a stream source is restricted (echo, expr, or exec).""" + return stream_source.strip().startswith(("echo:", "expr:", "exec:")) + + def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess: """Run ffprobe on stream.""" clean_path = escape_special_characters(path)