mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-31 19:00:30 +03:00
Compare commits
2
Commits
775ce22204
...
c2e739b4bc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2e739b4bc | ||
|
|
62d4e87e5d |
@@ -274,6 +274,13 @@ http {
|
|||||||
include proxy.conf;
|
include proxy.conf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /api/logout {
|
||||||
|
auth_request off;
|
||||||
|
rewrite ^/api(/.*)$ $1 break;
|
||||||
|
proxy_pass http://frigate_api;
|
||||||
|
include proxy.conf;
|
||||||
|
}
|
||||||
|
|
||||||
# Allow unauthenticated access to the first_time_login endpoint
|
# Allow unauthenticated access to the first_time_login endpoint
|
||||||
# so the login page can load help text before authentication.
|
# so the login page can load help text before authentication.
|
||||||
location /api/auth/first_time_login {
|
location /api/auth/first_time_login {
|
||||||
|
|||||||
+13
-5
@@ -415,7 +415,7 @@ def create_encoded_jwt(user, role, expiration, secret):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, expiration, secure):
|
def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, max_age, secure):
|
||||||
# TODO: ideally this would set secure as well, but that requires TLS
|
# TODO: ideally this would set secure as well, but that requires TLS
|
||||||
# SameSite is intentionally left unset (browsers default to Lax). Setting
|
# SameSite is intentionally left unset (browsers default to Lax). Setting
|
||||||
# SameSite=Lax/Strict would stop the cookie from being sent in cross-origin
|
# SameSite=Lax/Strict would stop the cookie from being sent in cross-origin
|
||||||
@@ -427,7 +427,7 @@ def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, expiration, sec
|
|||||||
key=cookie_name,
|
key=cookie_name,
|
||||||
value=encoded_jwt,
|
value=encoded_jwt,
|
||||||
httponly=True,
|
httponly=True,
|
||||||
expires=expiration,
|
max_age=max_age,
|
||||||
secure=secure,
|
secure=secure,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -762,7 +762,7 @@ def auth(request: Request):
|
|||||||
success_response,
|
success_response,
|
||||||
JWT_COOKIE_NAME,
|
JWT_COOKIE_NAME,
|
||||||
new_encoded_jwt,
|
new_encoded_jwt,
|
||||||
new_expiration,
|
JWT_SESSION_LENGTH,
|
||||||
JWT_COOKIE_SECURE,
|
JWT_COOKIE_SECURE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -875,7 +875,11 @@ def login(request: Request, body: AppPostLoginBody):
|
|||||||
encoded_jwt = create_encoded_jwt(user, role, expiration, request.app.jwt_token)
|
encoded_jwt = create_encoded_jwt(user, role, expiration, request.app.jwt_token)
|
||||||
response = Response("", 200)
|
response = Response("", 200)
|
||||||
set_jwt_cookie(
|
set_jwt_cookie(
|
||||||
response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE
|
response,
|
||||||
|
JWT_COOKIE_NAME,
|
||||||
|
encoded_jwt,
|
||||||
|
JWT_SESSION_LENGTH,
|
||||||
|
JWT_COOKIE_SECURE,
|
||||||
)
|
)
|
||||||
# Clear admin_first_time_login flag after successful admin login so the
|
# Clear admin_first_time_login flag after successful admin login so the
|
||||||
# UI stops showing the first-time login documentation link.
|
# UI stops showing the first-time login documentation link.
|
||||||
@@ -1037,7 +1041,11 @@ async def update_password(
|
|||||||
)
|
)
|
||||||
# Set new JWT cookie on response
|
# Set new JWT cookie on response
|
||||||
set_jwt_cookie(
|
set_jwt_cookie(
|
||||||
response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE
|
response,
|
||||||
|
JWT_COOKIE_NAME,
|
||||||
|
encoded_jwt,
|
||||||
|
JWT_SESSION_LENGTH,
|
||||||
|
JWT_COOKIE_SECURE,
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import re
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import regex
|
||||||
from playhouse.sqliteq import SqliteQueueDatabase
|
from playhouse.sqliteq import SqliteQueueDatabase
|
||||||
|
|
||||||
|
REGEXP_TIMEOUT_SECONDS = 1.0
|
||||||
|
|
||||||
|
|
||||||
class SqliteVecQueueDatabase(SqliteQueueDatabase):
|
class SqliteVecQueueDatabase(SqliteQueueDatabase):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -34,8 +36,10 @@ class SqliteVecQueueDatabase(SqliteQueueDatabase):
|
|||||||
if item is None:
|
if item is None:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
return re.search(expr, item) is not None
|
return (
|
||||||
except re.error:
|
regex.search(expr, item, timeout=REGEXP_TIMEOUT_SECONDS) is not None
|
||||||
|
)
|
||||||
|
except (regex.error, TimeoutError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
conn.create_function("REGEXP", 2, regexp)
|
conn.create_function("REGEXP", 2, regexp)
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Tests for the REGEXP function registered on the main Frigate database.
|
||||||
|
|
||||||
|
Regression coverage for GHSA-q8jx-q884-jcq9: an attacker-controlled
|
||||||
|
catastrophic (ReDoS) pattern reaching the REGEXP sink must not be able to
|
||||||
|
stall the serialized database worker thread.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from frigate.db.sqlitevecq import REGEXP_TIMEOUT_SECONDS, SqliteVecQueueDatabase
|
||||||
|
|
||||||
|
|
||||||
|
class TestRegexpFunction(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
# autostart=False keeps the queue worker thread from spinning up; we
|
||||||
|
# only need the REGEXP registration, exercised on our own connection.
|
||||||
|
self.db = SqliteVecQueueDatabase(":memory:", autostart=False)
|
||||||
|
self.conn = sqlite3.connect(":memory:")
|
||||||
|
self.db._register_regexp(self.conn)
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
self.conn.close()
|
||||||
|
|
||||||
|
def _regexp(self, value: str | None, pattern: str) -> int | None:
|
||||||
|
# SQLite maps "value REGEXP pattern" to regexp(pattern, value).
|
||||||
|
return self.conn.execute("SELECT ? REGEXP ?", (value, pattern)).fetchone()[0]
|
||||||
|
|
||||||
|
def test_normal_patterns_still_match(self) -> None:
|
||||||
|
self.assertTrue(self._regexp("ABC123", "^ABC"))
|
||||||
|
self.assertTrue(self._regexp("ABC123", "ABC.*"))
|
||||||
|
self.assertTrue(self._regexp("ABC123", "[0-9]+$"))
|
||||||
|
self.assertFalse(self._regexp("ABC123", "^XYZ"))
|
||||||
|
|
||||||
|
def test_null_value_does_not_match(self) -> None:
|
||||||
|
self.assertFalse(self._regexp(None, ".*"))
|
||||||
|
|
||||||
|
def test_invalid_pattern_does_not_raise(self) -> None:
|
||||||
|
self.assertFalse(self._regexp("ABC123", "(unclosed"))
|
||||||
|
|
||||||
|
def test_catastrophic_pattern_is_time_bounded(self) -> None:
|
||||||
|
# Without the timeout this evaluation backtracks for minutes to hours
|
||||||
|
# and wedges the whole database thread (GHSA-q8jx-q884-jcq9).
|
||||||
|
catastrophic = "(a{2,})+c"
|
||||||
|
subject = "a" * 4000
|
||||||
|
|
||||||
|
start = time.monotonic()
|
||||||
|
result = self._regexp(subject, catastrophic)
|
||||||
|
elapsed = time.monotonic() - start
|
||||||
|
|
||||||
|
# The pattern does not match; the guarantee is that it returns quickly.
|
||||||
|
self.assertFalse(result)
|
||||||
|
self.assertLess(elapsed, REGEXP_TIMEOUT_SECONDS + 2.0)
|
||||||
Reference in New Issue
Block a user