mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-15 16:31:14 +03:00
bound REGEXP evaluation with a timeout to prevent ReDoS on the database thread (#23714)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
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
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
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
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.
This commit is contained in:
parent
62d4e87e5d
commit
c2e739b4bc
@ -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
frigate/test/test_sqlitevecq_regexp.py
Normal file
54
frigate/test/test_sqlitevecq_regexp.py
Normal 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)
|
||||
Loading…
Reference in New Issue
Block a user