Compare commits

...
2 Commits
Author SHA1 Message Date
Josh HawkinsandGitHub c2e739b4bc bound REGEXP evaluation with a timeout to prevent ReDoS on the database thread (#23714)
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / AMD64 Extra Build (push) Blocked by required conditions
The recognized_license_plate event filter passed attacker-controlled patterns to re.search on the single serialized SQLite queue thread, letting any authenticated user freeze the whole application with a catastrophic regex. This swaps stdlib re for the regex module with a per-evaluation timeout so a pathological pattern is aborted instead of stalling every database operation.
2026-07-14 06:27:00 -05:00
nulledyandGitHub 62d4e87e5d Add logout endpoint to Nginx configuration to prevent a new token on logout (#23678)
* Add logout endpoint to Nginx configuration to prevent logout from silently generating a new frigate_token cookie

* Change JWT cookie expiration to use max_age and have the appropriate expiration time based on JWT_SESSION_LENGTH

* ruff formatting
2026-07-14 02:35:51 -08:00
4 changed files with 81 additions and 8 deletions
@@ -274,6 +274,13 @@ http {
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
# so the login page can load help text before authentication.
location /api/auth/first_time_login {
+13 -5
View File
@@ -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
# SameSite is intentionally left unset (browsers default to Lax). Setting
# 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,
value=encoded_jwt,
httponly=True,
expires=expiration,
max_age=max_age,
secure=secure,
)
@@ -762,7 +762,7 @@ def auth(request: Request):
success_response,
JWT_COOKIE_NAME,
new_encoded_jwt,
new_expiration,
JWT_SESSION_LENGTH,
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)
response = Response("", 200)
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
# UI stops showing the first-time login documentation link.
@@ -1037,7 +1041,11 @@ async def update_password(
)
# Set new JWT cookie on response
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
+7 -3
View File
@@ -1,9 +1,11 @@
import re
import sqlite3
from typing import Any
import regex
from playhouse.sqliteq import SqliteQueueDatabase
REGEXP_TIMEOUT_SECONDS = 1.0
class SqliteVecQueueDatabase(SqliteQueueDatabase):
def __init__(
@@ -34,8 +36,10 @@ class SqliteVecQueueDatabase(SqliteQueueDatabase):
if item is None:
return False
try:
return re.search(expr, item) is not None
except re.error:
return (
regex.search(expr, item, timeout=REGEXP_TIMEOUT_SECONDS) is not None
)
except (regex.error, TimeoutError):
return False
conn.create_function("REGEXP", 2, regexp)
+54
View File
@@ -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)