Skip to content

Commit 4f7c134

Browse files
authored
fix: verify webhook signatures against raw body first, fallback to JSON (#2392)
1 parent d4d8acd commit 4f7c134

File tree

1 file changed

+36
-6
lines changed

1 file changed

+36
-6
lines changed

api/ingest.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package api
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"errors"
67
"fmt"
@@ -157,17 +158,46 @@ func (a *ApplicationHandler) IngestEvent(w http.ResponseWriter, r *http.Request)
157158

158159
// 3.1 On Failure
159160
// Return 400 Bad Request.
160-
payload, err := extractPayloadFromIngestEventReq(r, maxIngestSize)
161+
// Read raw body for signature verification first (e.g., GitHub signs raw bytes)
162+
rawPayload, err := io.ReadAll(io.LimitReader(r.Body, int64(maxIngestSize)))
161163
if err != nil {
162-
a.A.Logger.WithError(err).Error("Failed to extract payload")
164+
a.A.Logger.WithError(err).Error("Failed to read request body")
163165
_ = render.Render(w, r, util.NewErrorResponse("Invalid request format", http.StatusBadRequest))
164166
return
165167
}
166168

167-
if err = v.VerifyRequest(r, payload); err != nil {
168-
a.A.Logger.WithError(err).Error("Request verification failed")
169-
_ = render.Render(w, r, util.NewErrorResponse("Request verification failed", http.StatusBadRequest))
170-
return
169+
// Restore body for subsequent reads
170+
r.Body = io.NopCloser(bytes.NewReader(rawPayload))
171+
172+
// Try raw-body verification first
173+
rawVerifyErr := v.VerifyRequest(r, rawPayload)
174+
175+
var payload []byte
176+
if rawVerifyErr != nil {
177+
// Fallback: extract/convert payload (e.g., form -> JSON) and verify against that for backward compatibility
178+
payload, err = extractPayloadFromIngestEventReq(r, maxIngestSize)
179+
if err != nil {
180+
a.A.Logger.WithError(err).Error("Failed to extract payload")
181+
_ = render.Render(w, r, util.NewErrorResponse("Invalid request format", http.StatusBadRequest))
182+
return
183+
}
184+
185+
// Reset body before verification (some verifiers may inspect headers/body state)
186+
r.Body = io.NopCloser(bytes.NewReader(rawPayload))
187+
188+
if err = v.VerifyRequest(r, payload); err != nil {
189+
a.A.Logger.WithError(rawVerifyErr).Error("Request verification failed (raw and converted)")
190+
_ = render.Render(w, r, util.NewErrorResponse("Request verification failed", http.StatusBadRequest))
191+
return
192+
}
193+
} else {
194+
// Raw verification succeeded; now convert for storage
195+
payload, err = extractPayloadFromIngestEventReq(r, maxIngestSize)
196+
if err != nil {
197+
a.A.Logger.WithError(err).Error("Failed to extract payload")
198+
_ = render.Render(w, r, util.NewErrorResponse("Invalid request format", http.StatusBadRequest))
199+
return
200+
}
171201
}
172202

173203
if len(payload) == 0 {

0 commit comments

Comments
 (0)