@marcel91200 / caddy Mirror · read-only / commits / 6cdffe95
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

requestbody: enforce max_size early using Content-Length (#8337)

* requestbody: reject oversized Content-Length before the next handler When max_size is set and the request declares a Content-Length above it, respond 413 right away instead of letting the next handler start and fail mid-read. Bodies of unknown length are still limited by MaxBytesReader. Both paths return the same error, so handle_errors sees no difference. * caddytest: send unknown-length bodies in max_size placeholder tests request_body now answers 413 for a declared Content-Length over max_size before the placeholder reads the body, so these tests no longer covered the read path and the direct respond one failed. * requestbody: keep the body limited for error routes Error routes get the same request, so wrap the body before the early 413 to stop them from reading past max_size.

Kévin Dunglas committed Nov 3, 2026 at 09:18 UTC 6cdffe9502f63d3539c82c29702858fc399f149e
3 files changed +122 -11
caddytest/integration/handler_test.go
+41 -11
@@ -77,11 +77,7 @@ func TestRequestBodyPlaceholderRespectsMaxSizeInTemplate(t *testing.T) {
77 }
78 `, "caddyfile")
79
80 - req, err := http.NewRequest(http.MethodPost, "http://localhost:9080/", bytes.NewBufferString("abcdefghijklm"))
81 - if err != nil {
82 - t.Fatalf("creating request: %v", err)
83 - }
84 - res := tester.AssertResponseCode(req, http.StatusRequestEntityTooLarge)
80 + res := tester.AssertResponseCode(newUnknownLengthPost(t, "abcdefghijklm"), http.StatusRequestEntityTooLarge)
81 res.Body.Close()
82 }
83
@@ -109,11 +105,7 @@ func TestRequestBodyPlaceholderRespectsMaxSizeInVarsRegexp(t *testing.T) {
105 }
106 `, "caddyfile")
107
112 - req, err := http.NewRequest(http.MethodPost, "http://localhost:9080/", bytes.NewBufferString("abcdefghijklmnopqrstuvwxyz"))
113 - if err != nil {
114 - t.Fatalf("creating request: %v", err)
115 - }
116 - res := tester.AssertResponseCode(req, http.StatusRequestEntityTooLarge)
108 + res := tester.AssertResponseCode(newUnknownLengthPost(t, "abcdefghijklmnopqrstuvwxyz"), http.StatusRequestEntityTooLarge)
109 res.Body.Close()
110 }
111
@@ -138,5 +130,43 @@ func TestRequestBodyPlaceholderDirectRespondExpansion(t *testing.T) {
130 // direct placeholder consumers expand the sanitized request-body limit
131 // marker to its text instead of failing the request with 413; only the
132 // templates and vars matchers propagate the 413 status
141 - tester.AssertPostResponseBody("http://localhost:9080/", nil, bytes.NewBufferString("abcdefghijklm"), http.StatusOK, "request body: http: request body too large")
133 + tester.AssertResponse(newUnknownLengthPost(t, "abcdefghijklm"), http.StatusOK, "request body: http: request body too large")
134 +}
135 +
136 +// newUnknownLengthPost sends no Content-Length, so the body gets past
137 +// request_body's early check and max_size trips while handlers read it.
138 +func newUnknownLengthPost(t *testing.T, body string) *http.Request {
139 + t.Helper()
140 + req, err := http.NewRequest(http.MethodPost, "http://localhost:9080/", bytes.NewBufferString(body))
141 + if err != nil {
142 + t.Fatalf("creating request: %v", err)
143 + }
144 + req.ContentLength = -1
145 + return req
146 +}
147 +
148 +func TestRequestBodyMaxSizeLimitsErrorRoute(t *testing.T) {
149 + tester := caddytest.NewTester(t)
150 + tester.InitServer(`
151 + {
152 + skip_install_trust
153 + admin localhost:2999
154 + http_port 9080
155 + https_port 9443
156 + grace_period 1ns
157 + }
158 + http://localhost:9080 {
159 + request_body {
160 + max_size 10
161 + }
162 + respond "next handler reached"
163 + handle_errors {
164 + respond "{http.request.body}" {err.status_code}
165 + }
166 + }
167 + `, "caddyfile")
168 +
169 + // the declared length is rejected before respond runs, and the error
170 + // route reading the body must still be held to max_size
171 + tester.AssertPostResponseBody("http://localhost:9080/", nil, bytes.NewBufferString("abcdefghijklm"), http.StatusRequestEntityTooLarge, "request body: http: request body too large")
172 }
modules/caddyhttp/requestbody/requestbody.go
+6
@@ -32,6 +32,8 @@ func init() {
32 type RequestBody struct {
33 // The maximum number of bytes to allow reading from the body by a later handler.
34 // If more bytes are read, an error with HTTP status 413 is returned.
35 + // Requests declaring a larger Content-Length get the same error
36 + // before any later handler runs.
37 MaxSize int64 `json:"max_size,omitempty"`
38
39 // This field permit to replace body on the fly
@@ -67,7 +69,11 @@ func (rb RequestBody) ServeHTTP(w http.ResponseWriter, r *http.Request, next cad
69 return next.ServeHTTP(w, r)
70 }
71 if rb.MaxSize > 0 {
72 + // wrap before rejecting: error routes get this request and may read the body
73 r.Body = errorWrapper{http.MaxBytesReader(w, r.Body, rb.MaxSize)}
74 + if r.ContentLength > rb.MaxSize {
75 + return caddyhttp.Error(http.StatusRequestEntityTooLarge, &http.MaxBytesError{Limit: rb.MaxSize})
76 + }
77 }
78 return next.ServeHTTP(w, r)
79 }
modules/caddyhttp/requestbody/requestbody_test.go
+75
@@ -16,9 +16,11 @@ package requestbody
16
17 import (
18 "context"
19 + "errors"
20 "io"
21 "net/http"
22 "net/http/httptest"
23 + "strings"
24 "testing"
25
26 "github.com/caddyserver/caddy/v2"
@@ -150,3 +152,76 @@ func TestRequestBodySetHeaderPlaceholders(t *testing.T) {
152 })
153 }
154 }
155 +
156 +// TestRequestBodyMaxSize verifies that a declared Content-Length over the limit
157 +// is rejected before the next handler runs, while bodies of unknown length still
158 +// fail on read, both with the same 413 error.
159 +func TestRequestBodyMaxSize(t *testing.T) {
160 + const maxSize = 8
161 +
162 + for _, tc := range []struct {
163 + name string
164 + body string
165 + contentLength int64
166 + wantNext bool
167 + wantErr bool
168 + }{
169 + {
170 + name: "declared length over the limit",
171 + body: "0123456789",
172 + contentLength: 10,
173 + wantErr: true,
174 + },
175 + {
176 + name: "declared length within the limit",
177 + body: "01234567",
178 + contentLength: 8,
179 + wantNext: true,
180 + },
181 + {
182 + name: "unknown length over the limit",
183 + body: "0123456789",
184 + contentLength: -1,
185 + wantNext: true,
186 + wantErr: true,
187 + },
188 + } {
189 + t.Run(tc.name, func(t *testing.T) {
190 + rb := RequestBody{MaxSize: maxSize}
191 +
192 + req := httptest.NewRequest(http.MethodPost, "https://example.com/upload", strings.NewReader(tc.body))
193 + req.ContentLength = tc.contentLength
194 + if tc.contentLength < 0 {
195 + req.TransferEncoding = []string{"chunked"}
196 + }
197 +
198 + var nextCalled bool
199 + next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
200 + nextCalled = true
201 + _, err := io.ReadAll(r.Body)
202 + return err
203 + })
204 +
205 + err := rb.ServeHTTP(httptest.NewRecorder(), req, next)
206 +
207 + if nextCalled != tc.wantNext {
208 + t.Errorf("next handler called = %t, want %t", nextCalled, tc.wantNext)
209 + }
210 +
211 + if !tc.wantErr {
212 + if err != nil {
213 + t.Fatalf("did not expect an error, but got: %v", err)
214 + }
215 + return
216 + }
217 +
218 + he, ok := errors.AsType[caddyhttp.HandlerError](err)
219 + if !ok || he.StatusCode != http.StatusRequestEntityTooLarge {
220 + t.Fatalf("want a %d handler error, got: %v", http.StatusRequestEntityTooLarge, err)
221 + }
222 + if mbe, ok := errors.AsType[*http.MaxBytesError](err); !ok || mbe.Limit != maxSize {
223 + t.Errorf("want a *http.MaxBytesError with limit %d, got: %v", maxSize, err)
224 + }
225 + })
226 + }
227 +}