From 37c78f3b99e29592f8657be1cf5950cb60626a3f Mon Sep 17 00:00:00 2001 From: YooLCD Date: Wed, 1 Apr 2026 19:22:17 +0900 Subject: [PATCH 1/2] limit webhook payload size in ValidatePayloadFromBody --- github/messages.go | 23 +++++++++++++--- github/messages_test.go | 58 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/github/messages.go b/github/messages.go index c3d14acdc64..9bb43c75df7 100644 --- a/github/messages.go +++ b/github/messages.go @@ -42,6 +42,10 @@ const ( EventTypeHeader = "X-Github-Event" // DeliveryIDHeader is the GitHub header key used to pass the unique ID for the webhook event. DeliveryIDHeader = "X-Github-Delivery" + + // maxPayloadSize is the maximum size of a GitHub webhook payload. + // GitHub documents a 25 MB limit for webhook payloads. + maxPayloadSize = 25 * 1024 * 1024 ) var ( @@ -146,8 +150,19 @@ func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool { return hmac.Equal(messageMAC, expectedMAC) } -// messageMAC returns the hex-decoded HMAC tag from the signature and its -// corresponding hash function. +// readPayloadBody reads the body from readable, enforcing maxPayloadSize. +func readPayloadBody(readable io.Reader) ([]byte, error) { + body, err := io.ReadAll(io.LimitReader(readable, maxPayloadSize+1)) + if err != nil { + return nil, err + } + if len(body) > maxPayloadSize { + return nil, errors.New("webhook payload exceeds maximum allowed size") + } + return body, nil +} + +// messageMAC returns the MAC method and the corresponding hash function. func messageMAC(signature string) ([]byte, func() hash.Hash, error) { if signature == "" { return nil, nil, errors.New("missing signature") @@ -199,7 +214,7 @@ func ValidatePayloadFromBody(contentType string, readable io.Reader, signature s switch contentType { case "application/json": var err error - if body, err = io.ReadAll(readable); err != nil { + if body, err = readPayloadBody(readable); err != nil { return nil, err } @@ -213,7 +228,7 @@ func ValidatePayloadFromBody(contentType string, readable io.Reader, signature s const payloadFormParam = "payload" var err error - if body, err = io.ReadAll(readable); err != nil { + if body, err = readPayloadBody(readable); err != nil { return nil, err } diff --git a/github/messages_test.go b/github/messages_test.go index 1d2d6ec5edb..5f1afb7fa07 100644 --- a/github/messages_test.go +++ b/github/messages_test.go @@ -10,6 +10,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "net/url" "strings" @@ -205,6 +206,16 @@ func (b *badReader) Read([]byte) (int, error) { func (b *badReader) Close() error { return errors.New("bad reader") } +// infiniteReader is an io.Reader that returns zeros indefinitely. +type infiniteReader struct{} + +func (infiniteReader) Read(p []byte) (int, error) { + for i := range p { + p[i] = 0 + } + return len(p), nil +} + func TestValidatePayload_BadRequestBody(t *testing.T) { t.Parallel() tests := []struct { @@ -228,6 +239,53 @@ func TestValidatePayload_BadRequestBody(t *testing.T) { } } +func TestValidatePayload_OversizedBody(t *testing.T) { + t.Parallel() + tests := []struct { + contentType string + }{ + {contentType: "application/json"}, + {contentType: "application/x-www-form-urlencoded"}, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("test #%v", i), func(t *testing.T) { + t.Parallel() + // Simulate a reader that reports more than maxPayloadSize bytes. + oversized := &fixedSizeReader{remaining: maxPayloadSize + 1} + req := &http.Request{ + Header: http.Header{"Content-Type": []string{tt.contentType}}, + Body: io.NopCloser(oversized), + } + _, err := ValidatePayload(req, nil) + if err == nil { + t.Fatal("ValidatePayload returned nil; want error for oversized body") + } + if want := "webhook payload exceeds maximum allowed size"; err.Error() != want { + t.Errorf("ValidatePayload error = %q, want %q", err.Error(), want) + } + }) + } +} + +// fixedSizeReader is an io.Reader that returns exactly remaining bytes, then EOF. +type fixedSizeReader struct { + remaining int64 +} + +func (r *fixedSizeReader) Read(p []byte) (int, error) { + if r.remaining == 0 { + return 0, io.EOF + } + + n := min(int64(len(p)), r.remaining) + for i := 0; i < int(n); i++ { + p[i] = 0 + } + r.remaining -= n + return int(n), nil +} + func TestValidatePayload_InvalidContentTypeParams(t *testing.T) { t.Parallel() req, err := http.NewRequest("POST", "http://localhost/event", nil) From 4034061e82af15cc72cb7c000742b641e41a6683 Mon Sep 17 00:00:00 2001 From: Yoo_LCD Date: Thu, 2 Apr 2026 00:18:53 +0900 Subject: [PATCH 2/2] fix messages_test.go --- github/messages_test.go | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/github/messages_test.go b/github/messages_test.go index 5f1afb7fa07..62bd47c6e1d 100644 --- a/github/messages_test.go +++ b/github/messages_test.go @@ -206,15 +206,9 @@ func (b *badReader) Read([]byte) (int, error) { func (b *badReader) Close() error { return errors.New("bad reader") } -// infiniteReader is an io.Reader that returns zeros indefinitely. -type infiniteReader struct{} +type readerFunc func([]byte) (int, error) -func (infiniteReader) Read(p []byte) (int, error) { - for i := range p { - p[i] = 0 - } - return len(p), nil -} +func (f readerFunc) Read(p []byte) (int, error) { return f(p) } func TestValidatePayload_BadRequestBody(t *testing.T) { t.Parallel() @@ -252,7 +246,12 @@ func TestValidatePayload_OversizedBody(t *testing.T) { t.Run(fmt.Sprintf("test #%v", i), func(t *testing.T) { t.Parallel() // Simulate a reader that reports more than maxPayloadSize bytes. - oversized := &fixedSizeReader{remaining: maxPayloadSize + 1} + oversized := io.LimitReader(readerFunc(func(p []byte) (int, error) { + for i := range p { + p[i] = 0 + } + return len(p), nil + }), maxPayloadSize+1) req := &http.Request{ Header: http.Header{"Content-Type": []string{tt.contentType}}, Body: io.NopCloser(oversized), @@ -268,24 +267,6 @@ func TestValidatePayload_OversizedBody(t *testing.T) { } } -// fixedSizeReader is an io.Reader that returns exactly remaining bytes, then EOF. -type fixedSizeReader struct { - remaining int64 -} - -func (r *fixedSizeReader) Read(p []byte) (int, error) { - if r.remaining == 0 { - return 0, io.EOF - } - - n := min(int64(len(p)), r.remaining) - for i := 0; i < int(n); i++ { - p[i] = 0 - } - r.remaining -= n - return int(n), nil -} - func TestValidatePayload_InvalidContentTypeParams(t *testing.T) { t.Parallel() req, err := http.NewRequest("POST", "http://localhost/event", nil)