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.