From 85311684d953dc07355e2974c4931850c612fb22 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Wed, 22 Apr 2026 16:10:34 +0300 Subject: [PATCH] fix(settings): don't clobber webhook secret with its mask on save MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /settings returns the Telegram webhook secret masked as "***". The frontend binds that masked value into its state, and any Save ships it back — the PUT handler then persisted the mask as the new secret, silently invalidating HMAC for every webhook-mode bot. The next GET re-masks the mask to itself, so the UI showed no corruption. Treat incoming values that begin with "***" as "unchanged" for the webhook-secret field. Empty strings still pass through (explicit clear). --- .../server/src/notify_bridge_server/api/app_settings.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/server/src/notify_bridge_server/api/app_settings.py b/packages/server/src/notify_bridge_server/api/app_settings.py index d37d17e..1515633 100644 --- a/packages/server/src/notify_bridge_server/api/app_settings.py +++ b/packages/server/src/notify_bridge_server/api/app_settings.py @@ -100,6 +100,13 @@ async def update_settings( if value is None: continue value_str = str(value) + # GET masks the webhook secret as "***" so the real value is + # never exposed to the frontend. If the client sends the mask back + # (which happens on every save, since bind:value holds whatever GET + # returned), treat it as "unchanged" — otherwise we'd overwrite the + # real secret with its mask, silently breaking webhook HMAC. + if key == "telegram_webhook_secret" and value_str.startswith("***"): + continue row = await session.get(AppSetting, key) if row: row.value = value_str