From 50819500f0ff42c1000c368f510602ddc36e100c Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Wed, 15 Apr 2026 21:23:12 -0700 Subject: [PATCH] fix(security): constant-time webhook_secret comparison (#337) Severity LOW. The /webhooks/:type handler compared the Telegram X-Telegram-Bot-Api-Secret-Token header against the decrypted webhook_secret using Go's `!=` operator, which short-circuits on the first mismatched byte. Under low-latency Docker-network conditions an attacker could time response latency byte-by-byte and converge on the real secret, then inject Telegram-formatted messages into any channel. Fix: switch to crypto/subtle.ConstantTimeCompare, which runs in time proportional to the length of the shorter input regardless of content match. Same posture as the cdp-proxy token compare in host-bridge (which already used timingSafeEqual). Risk profile over the public internet is low (Telegram webhooks have natural jitter that masks the signal), but the defensive pattern matters for consistency across all secret comparisons. Closes #337 Co-Authored-By: Claude Opus 4.6 (1M context) --- platform/internal/handlers/channels.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/platform/internal/handlers/channels.go b/platform/internal/handlers/channels.go index bcb7de73..e20432b2 100644 --- a/platform/internal/handlers/channels.go +++ b/platform/internal/handlers/channels.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "crypto/subtle" "database/sql" "encoding/json" "log" @@ -423,10 +424,17 @@ func (h *ChannelHandler) Webhook(c *gin.Context) { continue } - // Verify webhook secret_token if the channel has one configured + // Verify webhook secret_token if the channel has one configured. + // #337: use constant-time comparison. Go's `!=` short-circuits on + // the first mismatched byte and leaks timing information; an + // attacker on the Docker network could enumerate the secret + // byte-by-byte. subtle.ConstantTimeCompare runs in time + // proportional to the length of the shorter input and returns + // 1 on match / 0 otherwise (never -1). Same posture as the + // cdp-proxy token compare in host-bridge. if expectedSecret, _ := row.Config["webhook_secret"].(string); expectedSecret != "" { receivedSecret := c.GetHeader("X-Telegram-Bot-Api-Secret-Token") - if receivedSecret != expectedSecret { + if subtle.ConstantTimeCompare([]byte(receivedSecret), []byte(expectedSecret)) != 1 { continue // Wrong secret — try other channels (could be different bot) } }