From c06a97378c50e374936ea8c0810c152f6c732bea Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 9 Oct 2026 08:05:21 -0500 Subject: [PATCH] skip malformed webpush subscriptions instead of failing startup (#24604) Registration only checked that the p256dh and auth keys were non-empty strings, so any authenticated user could store a subscription that WebPusher refuses to construct. WebPushClient built one WebPusher per stored subscription with no error handling during init_dispatcher, so a single bad row raised before the camera processes and API started, and it did so again on every restart because the row was reloaded from the database. Building the pushers now logs and skips a subscription that fails to construct, both at startup and when expired subscriptions are cleaned up. --- frigate/comms/webpush.py | 25 ++++++++++++++++------- frigate/test/test_webpush_registration.py | 15 ++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/frigate/comms/webpush.py b/frigate/comms/webpush.py index 34a0e57508..45a6edba64 100644 --- a/frigate/comms/webpush.py +++ b/frigate/comms/webpush.py @@ -41,6 +41,20 @@ class PushNotification: ttl: int = 0 +def _build_web_pushers(user: str, subs: list[dict[str, Any]]) -> list[WebPusher]: + """Build pushers for a user's stored subscriptions, skipping unusable ones.""" + pushers: list[WebPusher] = [] + + for sub in subs: + # WebPusher decodes the stored keys and raises on malformed ones + try: + pushers.append(WebPusher(sub)) + except Exception: + logger.warning("Skipping invalid notification subscription for %s", user) + + return pushers + + class WebPushClient(Communicator): """Frigate wrapper for webpush client.""" @@ -82,9 +96,9 @@ class WebPushClient(Communicator): User.select(User.username, User.notification_tokens).dicts().iterator() ) for user in users: - self.web_pushers[user["username"]] = [] - for sub in user["notification_tokens"]: - self.web_pushers[user["username"]].append(WebPusher(sub)) + self.web_pushers[user["username"]] = _build_web_pushers( + user["username"], user["notification_tokens"] + ) # notification and auth config updater self.global_config_subscriber = ConfigSubscriber("config/") @@ -142,10 +156,7 @@ class WebPushClient(Communicator): User.username == user ).execute() - self.web_pushers[user] = [] - - for sub in user_subs: - self.web_pushers[user].append(WebPusher(sub)) + self.web_pushers[user] = _build_web_pushers(user, user_subs) logger.info( f"Cleaned up {len(expired)} notification subscriptions for {user}" diff --git a/frigate/test/test_webpush_registration.py b/frigate/test/test_webpush_registration.py index 16e0129152..733659489a 100644 --- a/frigate/test/test_webpush_registration.py +++ b/frigate/test/test_webpush_registration.py @@ -1,8 +1,10 @@ """Tests for push notification subscription validation.""" import unittest +from base64 import urlsafe_b64encode from frigate.api.notification import _validate_push_endpoint, _validate_subscription +from frigate.comms.webpush import _build_web_pushers VALID_ENDPOINTS = [ "https://fcm.googleapis.com/fcm/send/dGhpcy1pcy1hLXRva2Vu", @@ -148,3 +150,16 @@ class TestValidateSubscription(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +class TestBuildWebPushers(unittest.TestCase): + def test_skips_subscriptions_with_malformed_keys(self): + bad = _subscription(VALID_ENDPOINTS[0]) + good = _subscription(VALID_ENDPOINTS[0]) + good["keys"]["p256dh"] = urlsafe_b64encode(b"\x04" + bytes(64)).decode() + + with self.assertLogs("frigate.comms.webpush", level="WARNING"): + pushers = _build_web_pushers("viewer", [bad, good]) + + self.assertEqual(len(pushers), 1) + self.assertEqual(pushers[0].receiver_key, b"\x04" + bytes(64))