mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-14 07:51:14 +03:00
Compare commits
No commits in common. "35aee8cd4c51a15de4fa2cdafc3bc6a11c33b82c" and "8b340a09fa1d23590ea51ea9df1c7d4d90484c0c" have entirely different histories.
35aee8cd4c
...
8b340a09fa
@ -3,6 +3,7 @@
|
|||||||
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
|
||||||
@ -17,12 +18,37 @@ from frigate.const import (
|
|||||||
)
|
)
|
||||||
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
|
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
|
||||||
from frigate.util.config import find_config_file
|
from frigate.util.config import find_config_file
|
||||||
from frigate.util.services import is_restricted_go2rtc_source
|
|
||||||
|
|
||||||
sys.path.remove("/opt/frigate")
|
sys.path.remove("/opt/frigate")
|
||||||
|
|
||||||
yaml = YAML()
|
yaml = YAML()
|
||||||
|
|
||||||
|
# Check if arbitrary exec sources are allowed (defaults to False for security)
|
||||||
|
allow_arbitrary_exec = None
|
||||||
|
if "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.environ:
|
||||||
|
allow_arbitrary_exec = os.environ.get("GO2RTC_ALLOW_ARBITRARY_EXEC")
|
||||||
|
elif (
|
||||||
|
os.path.isdir("/run/secrets")
|
||||||
|
and os.access("/run/secrets", os.R_OK)
|
||||||
|
and "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.listdir("/run/secrets")
|
||||||
|
):
|
||||||
|
allow_arbitrary_exec = (
|
||||||
|
Path(os.path.join("/run/secrets", "GO2RTC_ALLOW_ARBITRARY_EXEC"))
|
||||||
|
.read_text()
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
# check for the add-on options file
|
||||||
|
elif os.path.isfile("/data/options.json"):
|
||||||
|
with open("/data/options.json") as f:
|
||||||
|
raw_options = f.read()
|
||||||
|
options = json.loads(raw_options)
|
||||||
|
allow_arbitrary_exec = options.get("go2rtc_allow_arbitrary_exec")
|
||||||
|
|
||||||
|
ALLOW_ARBITRARY_EXEC = allow_arbitrary_exec is not None and str(
|
||||||
|
allow_arbitrary_exec
|
||||||
|
).lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
|
|
||||||
config_file = find_config_file()
|
config_file = find_config_file()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -102,13 +128,18 @@ if LIBAVFORMAT_VERSION_MAJOR < 59:
|
|||||||
go2rtc_config["ffmpeg"]["rtsp"] = rtsp_args
|
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", {})):
|
for name in list(go2rtc_config.get("streams", {})):
|
||||||
stream = go2rtc_config["streams"][name]
|
stream = go2rtc_config["streams"][name]
|
||||||
|
|
||||||
if isinstance(stream, str):
|
if isinstance(stream, str):
|
||||||
try:
|
try:
|
||||||
formatted_stream = substitute_frigate_vars(stream)
|
formatted_stream = substitute_frigate_vars(stream)
|
||||||
if is_restricted_go2rtc_source(formatted_stream):
|
if not ALLOW_ARBITRARY_EXEC and is_restricted_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. "
|
||||||
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
|
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
|
||||||
@ -127,7 +158,7 @@ for name in list(go2rtc_config.get("streams", {})):
|
|||||||
for i, stream_item in enumerate(stream):
|
for i, stream_item in enumerate(stream):
|
||||||
try:
|
try:
|
||||||
formatted_stream = substitute_frigate_vars(stream_item)
|
formatted_stream = substitute_frigate_vars(stream_item)
|
||||||
if is_restricted_go2rtc_source(formatted_stream):
|
if not ALLOW_ARBITRARY_EXEC and is_restricted_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. "
|
||||||
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
|
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
|
||||||
|
|||||||
@ -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.camera_cleanup import cleanup_camera_db, cleanup_camera_files
|
||||||
from frigate.util.config import find_config_file
|
from frigate.util.config import find_config_file
|
||||||
from frigate.util.image import run_ffmpeg_snapshot
|
from frigate.util.image import run_ffmpeg_snapshot
|
||||||
from frigate.util.services import ffprobe_stream, is_restricted_go2rtc_source
|
from frigate.util.services import ffprobe_stream
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -147,24 +147,9 @@ def go2rtc_add_stream(request: Request, stream_name: str, src: str = ""):
|
|||||||
params = {"name": stream_name}
|
params = {"name": stream_name}
|
||||||
if src:
|
if src:
|
||||||
try:
|
try:
|
||||||
resolved_src = substitute_frigate_vars(src)
|
params["src"] = substitute_frigate_vars(src)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
resolved_src = src
|
params["src"] = src
|
||||||
|
|
||||||
if is_restricted_go2rtc_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(
|
r = requests.put(
|
||||||
"http://127.0.0.1:1984/api/streams",
|
"http://127.0.0.1:1984/api/streams",
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import os
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from fastapi import HTTPException, Request
|
from fastapi import HTTPException, Request
|
||||||
@ -358,51 +357,6 @@ class TestGo2rtcStreamAccess(BaseTestHttp):
|
|||||||
f"got {resp.status_code}"
|
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_add_stream_allows_restricted_source_when_override_set(self):
|
|
||||||
"""When GO2RTC_ALLOW_ARBITRARY_EXEC is set, the API must defer to operator
|
|
||||||
intent and forward the request to go2rtc instead of short-circuiting with 400."""
|
|
||||||
app = self._make_app(_MULTI_CAMERA_CONFIG)
|
|
||||||
mock_response = type("R", (), {"ok": True, "status_code": 200, "text": "ok"})()
|
|
||||||
with patch.dict(os.environ, {"GO2RTC_ALLOW_ARBITRARY_EXEC": "true"}):
|
|
||||||
with patch(
|
|
||||||
"frigate.api.camera.requests.put", return_value=mock_response
|
|
||||||
) as mock_put:
|
|
||||||
with AuthTestClient(app) as client:
|
|
||||||
resp = client.put("/go2rtc/streams/legit?src=exec:/tmp/something")
|
|
||||||
assert resp.status_code == 200, (
|
|
||||||
f"Restricted src should be forwarded when override set; got {resp.status_code}"
|
|
||||||
)
|
|
||||||
mock_put.assert_called_once()
|
|
||||||
forwarded_src = mock_put.call_args.kwargs["params"]["src"]
|
|
||||||
assert forwarded_src == "exec:/tmp/something"
|
|
||||||
|
|
||||||
def test_stream_alias_blocked_when_owning_camera_disallowed(self):
|
def test_stream_alias_blocked_when_owning_camera_disallowed(self):
|
||||||
"""limited_user cannot access a stream alias that belongs to a camera they
|
"""limited_user cannot access a stream alias that belongs to a camera they
|
||||||
are not allowed to see."""
|
are not allowed to see."""
|
||||||
|
|||||||
@ -778,41 +778,6 @@ def get_hailo_temps() -> dict[str, float]:
|
|||||||
return temps
|
return temps
|
||||||
|
|
||||||
|
|
||||||
def _go2rtc_arbitrary_exec_allowed() -> bool:
|
|
||||||
"""Read the GO2RTC_ALLOW_ARBITRARY_EXEC override from env, docker
|
|
||||||
secrets, or the Home Assistant add-on options file."""
|
|
||||||
raw: Optional[str] = None
|
|
||||||
if "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.environ:
|
|
||||||
raw = os.environ.get("GO2RTC_ALLOW_ARBITRARY_EXEC")
|
|
||||||
elif (
|
|
||||||
os.path.isdir("/run/secrets")
|
|
||||||
and os.access("/run/secrets", os.R_OK)
|
|
||||||
and "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.listdir("/run/secrets")
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
with open("/run/secrets/GO2RTC_ALLOW_ARBITRARY_EXEC") as f:
|
|
||||||
raw = f.read().strip()
|
|
||||||
except OSError:
|
|
||||||
raw = None
|
|
||||||
elif os.path.isfile("/data/options.json"):
|
|
||||||
try:
|
|
||||||
with open("/data/options.json") as f:
|
|
||||||
options = json.loads(f.read())
|
|
||||||
raw = options.get("go2rtc_allow_arbitrary_exec")
|
|
||||||
except (OSError, json.JSONDecodeError):
|
|
||||||
raw = None
|
|
||||||
|
|
||||||
return raw is not None and str(raw).lower() in ("true", "1", "yes")
|
|
||||||
|
|
||||||
|
|
||||||
def is_restricted_go2rtc_source(stream_source: str) -> bool:
|
|
||||||
"""Check if a stream source is a restricted type (echo, expr, or exec)
|
|
||||||
and the GO2RTC_ALLOW_ARBITRARY_EXEC override is not set."""
|
|
||||||
if not stream_source.strip().startswith(("echo:", "expr:", "exec:")):
|
|
||||||
return False
|
|
||||||
return not _go2rtc_arbitrary_exec_allowed()
|
|
||||||
|
|
||||||
|
|
||||||
def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess:
|
def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess:
|
||||||
"""Run ffprobe on stream."""
|
"""Run ffprobe on stream."""
|
||||||
clean_path = escape_special_characters(path)
|
clean_path = escape_special_characters(path)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user