From 20ec9ef9d3c93b685779ee8295129e0761ada7de Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:00:09 -0700 Subject: [PATCH] =?UTF-8?q?refactor(gateway/platforms):=20webhook.py=20?= =?UTF-8?q?=E2=80=94=20table-driven=20simple-provider=20signature=20checks?= =?UTF-8?q?=20(constant-time=20compare=20kept)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/platforms/webhook.py | 38 ++++++++++++++---------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/gateway/platforms/webhook.py b/gateway/platforms/webhook.py index 3aa1bf7ac0..6dd28fa3a3 100644 --- a/gateway/platforms/webhook.py +++ b/gateway/platforms/webhook.py @@ -1,14 +1,10 @@ -"""Generic webhook platform adapter: aiohttp server that validates HMAC-signed POSTs -(GitHub, GitLab, Svix, Linear, generic), renders payloads into agent prompts, and -routes responses back (github_comment or any gateway platform). - -Routes live under platforms.webhook.extra.routes: events (header filter), secret -(REQUIRED; "INSECURE_NO_AUTH" skips validation, loopback bind only), prompt template, -skills, deliver/deliver_extra, deliver_only (rendered prompt IS the message). Per-route -rate limiting, idempotency cache for provider retries, body-size caps checked before -reading. Generic HMAC V2 binds a timestamp for replay protection; body-only V1 is -deprecated but accepted with a warning. -""" +"""Generic webhook platform adapter: aiohttp server that validates HMAC-signed POSTs (GitHub, +GitLab, Svix, Linear, generic), renders payloads into agent prompts, and routes responses back +(github_comment or any gateway platform). Routes live under platforms.webhook.extra.routes: +events (header filter), secret (REQUIRED; "INSECURE_NO_AUTH" skips validation, loopback only), +prompt template, skills, deliver/deliver_extra, deliver_only (rendered prompt IS the message). +Per-route rate limiting, idempotency cache, body-size caps checked before reading. Generic HMAC +V2 binds a timestamp for replay protection; body-only V1 is deprecated but accepted with a warning.""" import asyncio import base64 @@ -678,18 +674,14 @@ class WebhookAdapter(BasePlatformAdapter): svix = [_header(name) for name in ("svix-id", "svix-timestamp", "svix-signature")] if any(svix): return _validate_svix_signature(body, secret, *svix) - # Linear: linear-signature = hex HMAC-SHA256 of the raw body (no timestamp binding). - linear_sig = _header("linear-signature") - if linear_sig: - return _hmac_str_equal(linear_sig, _hex_hmac(secret, body)) - # GitHub: X-Hub-Signature-256 = sha256= - gh_sig = headers.get("X-Hub-Signature-256", "") - if gh_sig: - return _hmac_str_equal(gh_sig, "sha256=" + _hex_hmac(secret, body)) - # GitLab: X-Gitlab-Token = - gl_token = headers.get("X-Gitlab-Token", "") - if gl_token: - return _hmac_str_equal(gl_token, secret) + # Linear (any header case): hex HMAC of the body. GitHub: sha256=. GitLab: plain token. + for provided, expected in ( + (_header("linear-signature"), lambda: _hex_hmac(secret, body)), + (headers.get("X-Hub-Signature-256", ""), lambda: "sha256=" + _hex_hmac(secret, body)), + (headers.get("X-Gitlab-Token", ""), lambda: secret), + ): + if provided: + return _hmac_str_equal(provided, expected()) route_name = request.match_info.get("route_name", "") # Generic V2: X-Webhook-Signature-V2 = hex HMAC-SHA256 of ".", # X-Webhook-Timestamp required. Presence of the V2 header COMMITS to V2 — it