From 1b322c8c81c18e0595d71a3ec39eaf6b6af11467 Mon Sep 17 00:00:00 2001 From: Aleksey Shakhmatov Date: Sun, 22 Mar 2026 21:47:26 +0300 Subject: [PATCH] Add server MaxBodySize middleware to prevent memory exhaustion Wraps request body with http.MaxBytesReader to limit incoming payload size. Without this, any endpoint accepting a body is vulnerable to large uploads consuming all available memory. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/middleware_bodylimit.go | 15 +++++++ server/middleware_bodylimit_test.go | 61 +++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 server/middleware_bodylimit.go create mode 100644 server/middleware_bodylimit_test.go diff --git a/server/middleware_bodylimit.go b/server/middleware_bodylimit.go new file mode 100644 index 0000000..85eabdc --- /dev/null +++ b/server/middleware_bodylimit.go @@ -0,0 +1,15 @@ +package server + +import "net/http" + +// MaxBodySize returns a middleware that limits the size of incoming request +// bodies. If the body exceeds n bytes, the server returns 413 Request Entity +// Too Large. It wraps the body with http.MaxBytesReader. +func MaxBodySize(n int64) Middleware { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, n) + next.ServeHTTP(w, r) + }) + } +} diff --git a/server/middleware_bodylimit_test.go b/server/middleware_bodylimit_test.go new file mode 100644 index 0000000..a8f7124 --- /dev/null +++ b/server/middleware_bodylimit_test.go @@ -0,0 +1,61 @@ +package server_test + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "git.codelab.vc/pkg/httpx/server" +) + +func TestMaxBodySize(t *testing.T) { + t.Run("allows body within limit", func(t *testing.T) { + handler := server.MaxBodySize(1024)( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusOK) + w.Write(b) + }), + ) + + body := strings.NewReader("hello") + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/", body) + handler.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("got status %d, want %d", w.Code, http.StatusOK) + } + if w.Body.String() != "hello" { + t.Fatalf("got body %q, want %q", w.Body.String(), "hello") + } + }) + + t.Run("rejects body exceeding limit", func(t *testing.T) { + handler := server.MaxBodySize(5)( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "body too large", http.StatusRequestEntityTooLarge) + return + } + w.WriteHeader(http.StatusOK) + }), + ) + + body := strings.NewReader("this is longer than 5 bytes") + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/", body) + handler.ServeHTTP(w, req) + + if w.Code != http.StatusRequestEntityTooLarge { + t.Fatalf("got status %d, want %d", w.Code, http.StatusRequestEntityTooLarge) + } + }) +}