Enforce the body size limit before CSRF parses the form (closes #90)
Some checks failed
check / check (push) Has been cancelled
Some checks failed
check / check (push) Has been cancelled
CSRF ran before MaxBodySize, so the CSRF middleware parsed the form body
before any cap applied and an oversized request was read in full before
being rejected. MaxBodySize is now the first middleware in all four route
groups that parse forms, ahead of CSRF and RequireAuth.
An oversize request therefore gets 413 without the handler running and
without state changing, including the password-change route.
Note the ordering trade: an unauthenticated client now receives 413 rather
than an auth redirect on /user/{username}/password.
This commit was merged in pull request #91.
This commit is contained in:
@@ -3,10 +3,12 @@ package middleware_test
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -648,6 +650,153 @@ func TestNoCache_SetsHeaders(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
// --- MaxBodySize Middleware Tests ---
|
||||
|
||||
const testBodyLimit int64 = 64
|
||||
|
||||
// maxBodySizeHandler wraps a sentinel handler in MaxBodySize with
|
||||
// testBodyLimit. The sentinel records whether it ran and how much of
|
||||
// the body it managed to read, so tests can distinguish "never
|
||||
// reached" from "reached but truncated".
|
||||
type maxBodySizeResult struct {
|
||||
called bool
|
||||
read int
|
||||
readErr error
|
||||
response *httptest.ResponseRecorder
|
||||
}
|
||||
|
||||
func runMaxBodySize(
|
||||
t *testing.T,
|
||||
req *http.Request,
|
||||
) *maxBodySizeResult {
|
||||
t.Helper()
|
||||
|
||||
m, _ := testMiddleware(t, config.EnvironmentDev)
|
||||
res := &maxBodySizeResult{response: httptest.NewRecorder()}
|
||||
|
||||
handler := m.MaxBodySize(testBodyLimit)(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
res.called = true
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
res.read = len(body)
|
||||
res.readErr = err
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
},
|
||||
))
|
||||
|
||||
handler.ServeHTTP(res.response, req)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// postWithBody builds a POST request whose Content-Length is
|
||||
// accurate for the given payload size.
|
||||
func postWithBody(size int) *http.Request {
|
||||
req := httptest.NewRequestWithContext(
|
||||
context.Background(),
|
||||
http.MethodPost, "/pages/login",
|
||||
strings.NewReader(strings.Repeat("a", size)),
|
||||
)
|
||||
req.Header.Set(
|
||||
"Content-Type", "application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func TestMaxBodySize_DeclaredOversize_413AndHandlerNotReached(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
res := runMaxBodySize(t, postWithBody(int(testBodyLimit)+1))
|
||||
|
||||
assert.False(
|
||||
t, res.called,
|
||||
"handler must not be reached for an oversized body",
|
||||
)
|
||||
assert.Equal(
|
||||
t, http.StatusRequestEntityTooLarge, res.response.Code,
|
||||
)
|
||||
}
|
||||
|
||||
func TestMaxBodySize_AtLimit_PassesThrough(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res := runMaxBodySize(t, postWithBody(int(testBodyLimit)))
|
||||
|
||||
assert.True(
|
||||
t, res.called,
|
||||
"handler should be reached for a body at the limit",
|
||||
)
|
||||
require.NoError(t, res.readErr)
|
||||
assert.Equal(t, int(testBodyLimit), res.read)
|
||||
assert.Equal(t, http.StatusOK, res.response.Code)
|
||||
}
|
||||
|
||||
func TestMaxBodySize_UnderLimit_PassesThrough(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res := runMaxBodySize(t, postWithBody(1))
|
||||
|
||||
assert.True(t, res.called)
|
||||
require.NoError(t, res.readErr)
|
||||
assert.Equal(t, 1, res.read)
|
||||
assert.Equal(t, http.StatusOK, res.response.Code)
|
||||
}
|
||||
|
||||
func TestMaxBodySize_GetWithOversizeBody_NotCapped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := httptest.NewRequestWithContext(
|
||||
context.Background(),
|
||||
http.MethodGet, "/pages/login",
|
||||
strings.NewReader(
|
||||
strings.Repeat("a", int(testBodyLimit)+1),
|
||||
),
|
||||
)
|
||||
|
||||
res := runMaxBodySize(t, req)
|
||||
|
||||
assert.True(
|
||||
t, res.called,
|
||||
"GET requests are not subject to the POST body cap",
|
||||
)
|
||||
require.NoError(t, res.readErr)
|
||||
assert.Equal(t, int(testBodyLimit)+1, res.read)
|
||||
}
|
||||
|
||||
// TestMaxBodySize_UndeclaredOversize_TruncatedAtCap covers the
|
||||
// chunked / lying-Content-Length case: there is nothing to check up
|
||||
// front, so the request reaches the handler but MaxBytesReader
|
||||
// hard-caps the body and the read fails at the limit.
|
||||
func TestMaxBodySize_UndeclaredOversize_TruncatedAtCap(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
req := postWithBody(int(testBodyLimit) + 1)
|
||||
// Simulate a chunked request: no declared length.
|
||||
req.ContentLength = -1
|
||||
|
||||
res := runMaxBodySize(t, req)
|
||||
|
||||
assert.True(
|
||||
t, res.called,
|
||||
"an undeclared oversize body cannot be rejected up front",
|
||||
)
|
||||
require.Error(
|
||||
t, res.readErr,
|
||||
"reading past the cap must fail",
|
||||
)
|
||||
assert.Equal(
|
||||
t, int(testBodyLimit), res.read,
|
||||
"the handler must not see more than the cap",
|
||||
)
|
||||
}
|
||||
|
||||
// --- Helper Tests ---
|
||||
|
||||
func TestIpFromHostPort(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user