mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-16 19:52:49 +03:00
Merge remote-tracking branch 'origin/master' into dev
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
This commit is contained in:
@@ -5,7 +5,7 @@ title: Updating
|
|||||||
|
|
||||||
# Updating Frigate
|
# Updating Frigate
|
||||||
|
|
||||||
The current stable version of Frigate is **0.18.1**. The release notes and any breaking changes for this version can be found on the [Frigate GitHub releases page](https://github.com/blakeblackshear/frigate/releases/tag/v0.18.1).
|
The current stable version of Frigate is **0.18.2**. The release notes and any breaking changes for this version can be found on the [Frigate GitHub releases page](https://github.com/blakeblackshear/frigate/releases/tag/v0.18.2).
|
||||||
|
|
||||||
Keeping Frigate up to date ensures you benefit from the latest features, performance improvements, and bug fixes. The update process varies slightly depending on your installation method (Docker, Home Assistant App, etc.). Below are instructions for the most common setups.
|
Keeping Frigate up to date ensures you benefit from the latest features, performance improvements, and bug fixes. The update process varies slightly depending on your installation method (Docker, Home Assistant App, etc.). Below are instructions for the most common setups.
|
||||||
|
|
||||||
@@ -31,21 +31,21 @@ If you’re running Frigate via Docker (recommended method), follow these steps:
|
|||||||
|
|
||||||
2. **Update and Pull the Latest Image**:
|
2. **Update and Pull the Latest Image**:
|
||||||
- If using Docker Compose:
|
- If using Docker Compose:
|
||||||
- Edit your `docker-compose.yml` file to specify the desired version tag (e.g., `0.18.1` instead of `0.17.1`). For example:
|
- Edit your `docker-compose.yml` file to specify the desired version tag (e.g., `0.18.2` instead of `0.17.1`). For example:
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
frigate:
|
frigate:
|
||||||
image: ghcr.io/blakeblackshear/frigate:0.18.1
|
image: ghcr.io/blakeblackshear/frigate:0.18.2
|
||||||
```
|
```
|
||||||
- Then pull the image:
|
- Then pull the image:
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/blakeblackshear/frigate:0.18.1
|
docker pull ghcr.io/blakeblackshear/frigate:0.18.2
|
||||||
```
|
```
|
||||||
- **Note for `stable` Tag Users**: If your `docker-compose.yml` uses the `stable` tag (e.g., `ghcr.io/blakeblackshear/frigate:stable`), you don’t need to update the tag manually. The `stable` tag always points to the latest stable release after pulling.
|
- **Note for `stable` Tag Users**: If your `docker-compose.yml` uses the `stable` tag (e.g., `ghcr.io/blakeblackshear/frigate:stable`), you don’t need to update the tag manually. The `stable` tag always points to the latest stable release after pulling.
|
||||||
- If using `docker run`:
|
- If using `docker run`:
|
||||||
- Pull the image with the appropriate tag (e.g., `0.18.1`, `0.18.1-tensorrt`, or `stable`):
|
- Pull the image with the appropriate tag (e.g., `0.18.2`, `0.18.2-tensorrt`, or `stable`):
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/blakeblackshear/frigate:0.18.1
|
docker pull ghcr.io/blakeblackshear/frigate:0.18.2
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Start the Container**:
|
3. **Start the Container**:
|
||||||
@@ -100,7 +100,7 @@ If an update causes issues:
|
|||||||
1. Stop Frigate.
|
1. Stop Frigate.
|
||||||
2. Restore your backed-up config file and database.
|
2. Restore your backed-up config file and database.
|
||||||
3. Revert to the previous image version:
|
3. Revert to the previous image version:
|
||||||
- For Docker: Specify an older tag (e.g., `ghcr.io/blakeblackshear/frigate:0.18.0`) in your `docker run` command.
|
- For Docker: Specify an older tag (e.g., `ghcr.io/blakeblackshear/frigate:0.18.1`) in your `docker run` command.
|
||||||
- For Docker Compose: Edit your `docker-compose.yml`, specify the older version tag (e.g., `ghcr.io/blakeblackshear/frigate:0.16.4`), and re-run `docker compose up -d`.
|
- For Docker Compose: Edit your `docker-compose.yml`, specify the older version tag (e.g., `ghcr.io/blakeblackshear/frigate:0.16.4`), and re-run `docker compose up -d`.
|
||||||
- For Home Assistant: Restore from the app/addon backup you took before you updated.
|
- For Home Assistant: Restore from the app/addon backup you took before you updated.
|
||||||
4. Verify the old version is running again.
|
4. Verify the old version is running again.
|
||||||
|
|||||||
+5
-34
@@ -12,7 +12,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import parse_qs, unquote
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||||
@@ -32,6 +32,8 @@ from frigate.api.media_auth import (
|
|||||||
check_camera_access,
|
check_camera_access,
|
||||||
deny_response_for_media_uri,
|
deny_response_for_media_uri,
|
||||||
is_role_restricted,
|
is_role_restricted,
|
||||||
|
normalize_request_path,
|
||||||
|
split_request_uri,
|
||||||
)
|
)
|
||||||
from frigate.config import AuthConfig, ProxyConfig
|
from frigate.config import AuthConfig, ProxyConfig
|
||||||
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
|
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
|
||||||
@@ -1276,37 +1278,6 @@ GO2RTC_STREAM_PROXY_PATHS = frozenset(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
_URI_SCHEME_AND_AUTHORITY = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*://[^/?]*")
|
|
||||||
|
|
||||||
|
|
||||||
def _split_request_uri(request_uri: str) -> tuple[str, str]:
|
|
||||||
"""Split a raw request URI into its path and query string.
|
|
||||||
|
|
||||||
urlparse is not used since it reads a path starting with `//` as a host.
|
|
||||||
"""
|
|
||||||
uri = _URI_SCHEME_AND_AUTHORITY.sub("", request_uri, count=1)
|
|
||||||
path, _, query = uri.partition("?")
|
|
||||||
return path, query
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_request_path(path: str) -> str:
|
|
||||||
"""Normalize a raw request path the way nginx does before matching locations.
|
|
||||||
|
|
||||||
nginx percent-decodes the path, merges repeated slashes and resolves dot
|
|
||||||
segments, while `X-Original-URL` carries the raw `$request_uri`.
|
|
||||||
"""
|
|
||||||
segments: list[str] = []
|
|
||||||
for segment in unquote(path).split("/"):
|
|
||||||
if segment in ("", "."):
|
|
||||||
continue
|
|
||||||
if segment == "..":
|
|
||||||
if segments:
|
|
||||||
segments.pop()
|
|
||||||
continue
|
|
||||||
segments.append(segment)
|
|
||||||
|
|
||||||
return "/" + "/".join(segments)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_go2rtc_stream_proxy_request(path: str, request: Request) -> bool:
|
def _is_go2rtc_stream_proxy_request(path: str, request: Request) -> bool:
|
||||||
# nginx marks requests routed through the go2rtc stream locations, and the
|
# nginx marks requests routed through the go2rtc stream locations, and the
|
||||||
@@ -1314,7 +1285,7 @@ def _is_go2rtc_stream_proxy_request(path: str, request: Request) -> bool:
|
|||||||
if request.headers.get("x-go2rtc-stream-proxy") == "1":
|
if request.headers.get("x-go2rtc-stream-proxy") == "1":
|
||||||
return True
|
return True
|
||||||
|
|
||||||
normalized = _normalize_request_path(path)
|
normalized = normalize_request_path(path)
|
||||||
return any(normalized.startswith(p) for p in GO2RTC_STREAM_PROXY_PATHS)
|
return any(normalized.startswith(p) for p in GO2RTC_STREAM_PROXY_PATHS)
|
||||||
|
|
||||||
|
|
||||||
@@ -1333,7 +1304,7 @@ def deny_response_for_go2rtc_stream(
|
|||||||
if not original_url:
|
if not original_url:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
path, query = _split_request_uri(original_url)
|
path, query = split_request_uri(original_url)
|
||||||
if not _is_go2rtc_stream_proxy_request(path, request):
|
if not _is_go2rtc_stream_proxy_request(path, request):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
+42
-10
@@ -13,8 +13,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from urllib.parse import unquote, urlparse
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from peewee import DoesNotExist
|
from peewee import DoesNotExist
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ from frigate.models import Export, User
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_URI_SCHEME_AND_AUTHORITY = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*://[^/?]*")
|
||||||
|
|
||||||
|
|
||||||
class MediaAuthResolution(str, Enum):
|
class MediaAuthResolution(str, Enum):
|
||||||
"""Classification of an `X-Original-URL` path for media-auth purposes."""
|
"""Classification of an `X-Original-URL` path for media-auth purposes."""
|
||||||
@@ -41,6 +44,36 @@ class MediaAuthResolution(str, Enum):
|
|||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def split_request_uri(request_uri: str) -> tuple[str, str]:
|
||||||
|
"""Split a raw request URI into its path and query string.
|
||||||
|
|
||||||
|
urlparse is not used since it reads a path starting with `//` as a host.
|
||||||
|
nginx ends the URI at a literal `#`, so anything after it is dropped.
|
||||||
|
"""
|
||||||
|
uri = _URI_SCHEME_AND_AUTHORITY.sub("", request_uri, count=1)
|
||||||
|
path, _, query = uri.partition("#")[0].partition("?")
|
||||||
|
return path, query
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_request_path(path: str) -> str:
|
||||||
|
"""Normalize a raw request path the way nginx does before matching locations.
|
||||||
|
|
||||||
|
nginx percent-decodes the path, merges repeated slashes and resolves dot
|
||||||
|
segments, while `X-Original-URL` carries the raw `$request_uri`.
|
||||||
|
"""
|
||||||
|
segments: list[str] = []
|
||||||
|
for segment in unquote(path).split("/"):
|
||||||
|
if segment in ("", "."):
|
||||||
|
continue
|
||||||
|
if segment == "..":
|
||||||
|
if segments:
|
||||||
|
segments.pop()
|
||||||
|
continue
|
||||||
|
segments.append(segment)
|
||||||
|
|
||||||
|
return "/" + "/".join(segments)
|
||||||
|
|
||||||
|
|
||||||
def extract_path(original_url: str | None) -> str | None:
|
def extract_path(original_url: str | None) -> str | None:
|
||||||
"""Return the decoded path component of nginx's `X-Original-URL` header.
|
"""Return the decoded path component of nginx's `X-Original-URL` header.
|
||||||
|
|
||||||
@@ -49,14 +82,14 @@ def extract_path(original_url: str | None) -> str | None:
|
|||||||
request like `/recordings/.../allowed_cam/../forbidden_cam/file.mp4`
|
request like `/recordings/.../allowed_cam/../forbidden_cam/file.mp4`
|
||||||
would (1) parse as the allowed camera in our auth check, (2) be served
|
would (1) parse as the allowed camera in our auth check, (2) be served
|
||||||
as the forbidden camera by nginx. To close the bypass we reject any URI
|
as the forbidden camera by nginx. To close the bypass we reject any URI
|
||||||
whose path contains `.` or `..` segments outright.
|
whose path contains `.` or `..` segments outright. Repeated slashes are
|
||||||
|
merged, as nginx does before matching locations.
|
||||||
"""
|
"""
|
||||||
if not original_url:
|
if not original_url:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
parsed = urlparse(original_url)
|
path, _ = split_request_uri(original_url)
|
||||||
raw_path = parsed.path or original_url
|
decoded = re.sub(r"/{2,}", "/", unquote(path))
|
||||||
decoded = unquote(raw_path)
|
|
||||||
if not decoded:
|
if not decoded:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -252,12 +285,11 @@ def deny_response_for_media_uri(
|
|||||||
path = extract_path(original_url)
|
path = extract_path(original_url)
|
||||||
|
|
||||||
# `extract_path` returns None for URIs containing `.` or `..` segments.
|
# `extract_path` returns None for URIs containing `.` or `..` segments.
|
||||||
# For media-root URIs that's a traversal attempt — deny outright. For
|
# Deny them when nginx would resolve them under a media root, and pass
|
||||||
# non-media URIs, pass through (nginx / the backend handle them).
|
# the rest through to nginx and the backend.
|
||||||
if path is None:
|
if path is None:
|
||||||
raw = urlparse(original_url).path or original_url
|
normalized = normalize_request_path(split_request_uri(original_url)[0])
|
||||||
decoded = unquote(raw)
|
first = normalized.lstrip("/").split("/", 1)[0]
|
||||||
first = decoded.lstrip("/").split("/", 1)[0] if decoded else ""
|
|
||||||
if first in ("clips", "recordings", "exports"):
|
if first in ("clips", "recordings", "exports"):
|
||||||
return 403
|
return 403
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -76,6 +76,20 @@ class TestExtractPath(unittest.TestCase):
|
|||||||
self.assertIsNone(extract_path(None))
|
self.assertIsNone(extract_path(None))
|
||||||
self.assertIsNone(extract_path(""))
|
self.assertIsNone(extract_path(""))
|
||||||
|
|
||||||
|
def test_leading_double_slash_is_path(self):
|
||||||
|
self.assertEqual(
|
||||||
|
extract_path("//clips/front_door-1.jpg"), "/clips/front_door-1.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_repeated_slashes_merged(self):
|
||||||
|
self.assertEqual(
|
||||||
|
extract_path("/recordings//2026-05-11/14//front_door/00.00.mp4"),
|
||||||
|
"/recordings/2026-05-11/14/front_door/00.00.mp4",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
extract_path("/%2Fclips/front_door-1.jpg"), "/clips/front_door-1.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestResolveMediaUri(unittest.TestCase):
|
class TestResolveMediaUri(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -376,6 +390,34 @@ class TestDenyResponseForMediaUri(unittest.TestCase):
|
|||||||
self.assertIsNone(self._deny("/clips/nonexistent-1.jpg", "admin"))
|
self.assertIsNone(self._deny("/clips/nonexistent-1.jpg", "admin"))
|
||||||
self.assertIsNone(self._deny("/clips/nonexistent-1.jpg", "viewer"))
|
self.assertIsNone(self._deny("/clips/nonexistent-1.jpg", "viewer"))
|
||||||
|
|
||||||
|
def test_repeated_slashes_resolve_to_owning_camera(self):
|
||||||
|
# nginx merges repeated slashes before matching the media locations
|
||||||
|
for url in (
|
||||||
|
"//clips/back_door-1.jpg",
|
||||||
|
"/clips//back_door-1.jpg",
|
||||||
|
"//recordings/2026-05-11/14/back_door/00.00.mp4",
|
||||||
|
"/recordings//2026-05-11/14/back_door/00.00.mp4",
|
||||||
|
):
|
||||||
|
self.assertEqual(self._deny(url, "limited_user"), 403, url)
|
||||||
|
self.assertIsNone(self._deny("//clips/front_door-1.jpg", "limited_user"))
|
||||||
|
|
||||||
|
def test_traversal_with_leading_double_slash_denied(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self._deny("//clips/front_door/../back_door-1.jpg", "admin"), 403
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_traversal_from_non_media_prefix_denied(self):
|
||||||
|
# nginx resolves these into a media root before serving
|
||||||
|
for url in (
|
||||||
|
"/x/../clips/back_door-1.jpg",
|
||||||
|
"/x/%2E%2E/clips/back_door-1.jpg",
|
||||||
|
"/./clips/front_door/../back_door-1.jpg",
|
||||||
|
"/api/../recordings/2026-05-11/14/back_door/00.00.mp4",
|
||||||
|
"/clips/back_door-1.jpg#/../../x",
|
||||||
|
):
|
||||||
|
self.assertEqual(self._deny(url, "limited_user"), 403, url)
|
||||||
|
self.assertIsNone(self._deny("/x/../api/events", "limited_user"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user