From 94c5a725d4fcb4953aa5f78be5ccda2e914f26a1 Mon Sep 17 00:00:00 2001 From: nikita42 Date: Wed, 8 Apr 2026 00:21:11 +0300 Subject: [PATCH] Harden webhook example request validation --- examples/webhook/main.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/examples/webhook/main.go b/examples/webhook/main.go index c5551bd..1b39f30 100644 --- a/examples/webhook/main.go +++ b/examples/webhook/main.go @@ -1,6 +1,7 @@ package main import ( + "crypto/subtle" "encoding/json" "fmt" "io" @@ -17,12 +18,17 @@ import ( // Minimal webhook receiver: starts HTTP server, parses updates from YM and replies via SendToChat. // Env: // YM_TOKEN (required), YM_REPLY_CHAT (optional default from incoming update), +// YM_WEBHOOK_SECRET (required, value expected in X-Webhook-Secret header), // YM_PORT (default 8080). func main() { token := os.Getenv("YM_TOKEN") if token == "" { log.Fatal("YM_TOKEN is required") } + webhookSecret := os.Getenv("YM_WEBHOOK_SECRET") + if webhookSecret == "" { + log.Fatal("YM_WEBHOOK_SECRET is required") + } port := os.Getenv("YM_PORT") if port == "" { port = "8080" @@ -35,7 +41,19 @@ func main() { http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - body, _ := io.ReadAll(r.Body) + if subtle.ConstantTimeCompare([]byte(r.Header.Get("X-Webhook-Secret")), []byte(webhookSecret)) != 1 { + http.Error(w, "unauthorized", http.StatusUnauthorized) + + return + } + + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + + return + } var upd ym.Update if err := json.Unmarshal(body, &upd); err != nil { @@ -61,7 +79,7 @@ func main() { } w.WriteHeader(http.StatusOK) - _, err := w.Write([]byte(`{"ok":true}`)) + _, err = w.Write([]byte(`{"ok":true}`)) if err != nil { log.Printf("write failed: %v", err)