diff --git a/pkg/diunwebhook/diunwebhook.go b/pkg/diunwebhook/diunwebhook.go index 4711c29..a98388d 100644 --- a/pkg/diunwebhook/diunwebhook.go +++ b/pkg/diunwebhook/diunwebhook.go @@ -4,6 +4,7 @@ import ( "crypto/subtle" "database/sql" "encoding/json" + "errors" "log" "net/http" "strconv" @@ -14,6 +15,8 @@ import ( _ "modernc.org/sqlite" ) +const maxBodyBytes = 1 << 20 // 1 MB + type DiunEvent struct { DiunVersion string `json:"diun_version"` Hostname string `json:"hostname"` @@ -199,9 +202,15 @@ func WebhookHandler(w http.ResponseWriter, r *http.Request) { return } + r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes) var event DiunEvent if err := json.NewDecoder(r.Body).Decode(&event); err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + http.Error(w, "request body too large", http.StatusRequestEntityTooLarge) + return + } log.Printf("WebhookHandler: failed to decode request: %v", err) http.Error(w, "bad request", http.StatusBadRequest) return @@ -296,10 +305,20 @@ func TagsHandler(w http.ResponseWriter, r *http.Request) { } case http.MethodPost: + r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes) var req struct { Name string `json:"name"` } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" { + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + http.Error(w, "request body too large", http.StatusRequestEntityTooLarge) + return + } + http.Error(w, "bad request: name required", http.StatusBadRequest) + return + } + if req.Name == "" { http.Error(w, "bad request: name required", http.StatusBadRequest) return } @@ -358,11 +377,21 @@ func TagByIDHandler(w http.ResponseWriter, r *http.Request) { func TagAssignmentHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPut: + r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes) var req struct { Image string `json:"image"` TagID int `json:"tag_id"` } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Image == "" { + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + http.Error(w, "request body too large", http.StatusRequestEntityTooLarge) + return + } + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if req.Image == "" { http.Error(w, "bad request", http.StatusBadRequest) return } @@ -383,10 +412,20 @@ func TagAssignmentHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) case http.MethodDelete: + r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes) var req struct { Image string `json:"image"` } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Image == "" { + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + http.Error(w, "request body too large", http.StatusRequestEntityTooLarge) + return + } + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if req.Image == "" { http.Error(w, "bad request", http.StatusBadRequest) return } diff --git a/pkg/diunwebhook/diunwebhook_test.go b/pkg/diunwebhook/diunwebhook_test.go index 3160aca..3271830 100644 --- a/pkg/diunwebhook/diunwebhook_test.go +++ b/pkg/diunwebhook/diunwebhook_test.go @@ -614,11 +614,12 @@ func TestGetUpdates_IncludesTag(t *testing.T) { } func TestWebhookHandler_OversizedBody(t *testing.T) { - // Generate a body that exceeds 1 MB (maxBodyBytes = 1<<20 = 1,048,576 bytes) - oversized := make([]byte, 1<<20+1) - for i := range oversized { - oversized[i] = 'x' - } + // Generate a body that exceeds 1 MB (maxBodyBytes = 1<<20 = 1,048,576 bytes). + // Use a valid JSON prefix so the decoder reads past the limit before failing, + // ensuring MaxBytesReader triggers a 413 rather than a JSON parse 400. + prefix := []byte(`{"image":"`) + padding := bytes.Repeat([]byte("x"), 1<<20+1) + oversized := append(prefix, padding...) req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(oversized)) rec := httptest.NewRecorder() diun.WebhookHandler(rec, req) @@ -628,10 +629,9 @@ func TestWebhookHandler_OversizedBody(t *testing.T) { } func TestTagsHandler_OversizedBody(t *testing.T) { - oversized := make([]byte, 1<<20+1) - for i := range oversized { - oversized[i] = 'x' - } + prefix := []byte(`{"name":"`) + padding := bytes.Repeat([]byte("x"), 1<<20+1) + oversized := append(prefix, padding...) req := httptest.NewRequest(http.MethodPost, "/api/tags", bytes.NewReader(oversized)) rec := httptest.NewRecorder() diun.TagsHandler(rec, req) @@ -641,10 +641,9 @@ func TestTagsHandler_OversizedBody(t *testing.T) { } func TestTagAssignmentHandler_OversizedBody(t *testing.T) { - oversized := make([]byte, 1<<20+1) - for i := range oversized { - oversized[i] = 'x' - } + prefix := []byte(`{"image":"`) + padding := bytes.Repeat([]byte("x"), 1<<20+1) + oversized := append(prefix, padding...) req := httptest.NewRequest(http.MethodPut, "/api/tag-assignments", bytes.NewReader(oversized)) rec := httptest.NewRecorder() diun.TagAssignmentHandler(rec, req)