From 708b57e009fc1819f00bd2c9ad4ace0091473a0c Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 4 Jul 2026 14:17:18 -0700 Subject: [PATCH] fix(webhook): rate-limit V1 deprecation warning + document V2 signature - warn once per route instead of on every request (busy senders would spam the log) - document X-Webhook-Signature-V2 / X-Webhook-Timestamp in the webhooks user guide Follow-ups for salvaged #58461. --- gateway/platforms/webhook.py | 22 ++++++++++++------- website/docs/user-guide/messaging/webhooks.md | 3 ++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/gateway/platforms/webhook.py b/gateway/platforms/webhook.py index a53697e30e7..f6080c57dcd 100644 --- a/gateway/platforms/webhook.py +++ b/gateway/platforms/webhook.py @@ -121,6 +121,9 @@ class WebhookAdapter(BasePlatformAdapter): self._dynamic_routes_mtime: float = 0.0 self._routes: Dict[str, dict] = dict(self._static_routes) self._runner = None + # Routes already warned about legacy V1 body-only signatures + # (once-per-route so a busy sender doesn't spam the log). + self._v1_signature_warned: set[str] = set() # Delivery info keyed by session chat_id. # @@ -913,14 +916,17 @@ class WebhookAdapter(BasePlatformAdapter): expected = hmac.new( secret.encode(), body, hashlib.sha256 ).hexdigest() - logger.warning( - "[webhook] Route '%s' uses legacy body-only HMAC (no " - "timestamp), which is vulnerable to replay attacks. Add " - "an 'X-Webhook-Timestamp' header and switch to " - "'X-Webhook-Signature-V2' (HMAC-SHA256 of " - "'.').", - request.match_info.get("route_name", ""), - ) + route_name = request.match_info.get("route_name", "") + if route_name not in self._v1_signature_warned: + self._v1_signature_warned.add(route_name) + logger.warning( + "[webhook] Route '%s' uses legacy body-only HMAC (no " + "timestamp), which is vulnerable to replay attacks. Add " + "an 'X-Webhook-Timestamp' header and switch to " + "'X-Webhook-Signature-V2' (HMAC-SHA256 of " + "'.').", + route_name, + ) return hmac.compare_digest(generic_sig, expected) # No recognised signature header but secret is configured → reject diff --git a/website/docs/user-guide/messaging/webhooks.md b/website/docs/user-guide/messaging/webhooks.md index 98e8d066056..0a8296f0dc5 100644 --- a/website/docs/user-guide/messaging/webhooks.md +++ b/website/docs/user-guide/messaging/webhooks.md @@ -387,7 +387,8 @@ The adapter validates incoming webhook signatures using the appropriate method f - **GitHub**: `X-Hub-Signature-256` header — HMAC-SHA256 hex digest prefixed with `sha256=` - **GitLab**: `X-Gitlab-Token` header — plain secret string match -- **Generic**: `X-Webhook-Signature` header — raw HMAC-SHA256 hex digest +- **Generic (V2, recommended)**: `X-Webhook-Signature-V2` + `X-Webhook-Timestamp` headers — HMAC-SHA256 hex digest of `.`. The timestamp (Unix seconds) must be within ±300 seconds of the server clock, which prevents captured requests from being replayed later. +- **Generic (V1, legacy)**: `X-Webhook-Signature` header — raw HMAC-SHA256 hex digest of the body only. Still accepted for backward compatibility, but it has no replay protection (a captured request replays indefinitely); the gateway logs a deprecation warning once per route. Switch senders to V2. If a secret is configured but no recognized signature header is present, the request is rejected.