Enforce the body size limit before CSRF parses the form (closes #90)
All checks were successful
check / check (push) Successful in 3m6s

chi runs Use middleware in registration order, and every form route
group registered CSRF() before MaxBodySize(). gorilla/csrf calls
r.PostFormValue, so the form was parsed under net/http's 10 MB default
and the intended 1 MB cap never applied to form fields. The
/user/{username} group, which carries POST /password, had no
MaxBodySize registration at all.

- Register MaxBodySize ahead of CSRF in /pages, /sources, and
  /source/{sourceID}, and add it to /user/{username}.
- Reject a declared-oversize body up front with 413. Reordering alone
  cannot produce one: http.MaxBytesReader surfaces its error on Read,
  so the form parse fails and gorilla/csrf answers 403 "no token" for
  what is really an oversized body. MaxBytesReader is still installed
  afterwards so chunked or length-lying clients stay hard-capped.
- Drop the handler-local MaxBytesReader calls in auth.go, profile.go,
  and source_management.go now that the middleware is the single
  enforcement point. maxBodyShift stays; webhook.go still uses it.

The /webhook/{uuid} receiver is untouched: it bounds itself with
io.LimitReader in readWebhookBody and is neither CSRF-protected nor
form-parsed.

Tests cover the middleware in isolation (declared oversize is rejected
without reaching a sentinel handler; at-limit and under-limit bodies
pass through intact; GET is unaffected; an undeclared oversize body is
truncated at the cap) and the real router built by SetupRoutes, so the
registration order itself is guarded: an oversized POST to
/pages/login returns 413 with no gorilla/csrf cookie issued, an
oversized POST /password with a valid session and CSRF token returns
413 and leaves the stored hash unchanged, and under-limit requests
still complete through the normal CSRF path.
This commit is contained in:
2026-08-09 01:53:34 +00:00
parent 4f5ecb18e5
commit 08c9c1a5d8
10 changed files with 661 additions and 48 deletions

View File

@@ -29,10 +29,8 @@ func (h *Handlers) HandleLoginPage() http.HandlerFunc {
// HandleLoginSubmit handles the login form submission (POST)
func (h *Handlers) HandleLoginSubmit() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Limit request body to prevent memory exhaustion
r.Body = http.MaxBytesReader(w, r.Body, 1<<maxBodyShift)
// Parse form data
// The body size cap is enforced by the MaxBodySize
// middleware, which runs before CSRF parses the form.
err := r.ParseForm()
if err != nil {
h.log.Error("failed to parse form", "error", err)

View File

@@ -31,9 +31,8 @@ func (h *Handlers) HandlePasswordChange() http.HandlerFunc {
return
}
// Limit request body to prevent memory exhaustion.
r.Body = http.MaxBytesReader(w, r.Body, 1<<maxBodyShift)
// The body size cap is enforced by the MaxBodySize
// middleware, which runs before CSRF parses the form.
err := r.ParseForm()
if err != nil {
h.log.Error("failed to parse form", "error", err)

View File

@@ -127,10 +127,8 @@ func (h *Handlers) HandleSourceCreateSubmit() http.HandlerFunc {
return
}
r.Body = http.MaxBytesReader(
w, r.Body, 1<<maxBodyShift,
)
// The body size cap is enforced by the MaxBodySize
// middleware, which runs before CSRF parses the form.
err := r.ParseForm()
if err != nil {
http.Error(
@@ -386,10 +384,8 @@ func (h *Handlers) HandleSourceEditSubmit() http.HandlerFunc {
return
}
r.Body = http.MaxBytesReader(
w, r.Body, 1<<maxBodyShift,
)
// The body size cap is enforced by the MaxBodySize
// middleware, which runs before CSRF parses the form.
err = r.ParseForm()
if err != nil {
http.Error(
@@ -409,10 +405,8 @@ func (h *Handlers) applyWebhookEdit(
r *http.Request,
webhook *database.Webhook,
) {
r.Body = http.MaxBytesReader(
w, r.Body, 1<<maxBodyShift,
)
// The body size cap is enforced by the MaxBodySize middleware,
// which runs before CSRF parses the form.
name := r.FormValue("name")
if name == "" {
data := map[string]any{
@@ -725,10 +719,8 @@ func (h *Handlers) HandleEntrypointCreate() http.HandlerFunc {
return
}
r.Body = http.MaxBytesReader(
w, r.Body, 1<<maxBodyShift,
)
// The body size cap is enforced by the MaxBodySize
// middleware, which runs before CSRF parses the form.
err = r.ParseForm()
if err != nil {
http.Error(
@@ -785,10 +777,8 @@ func (h *Handlers) HandleTargetCreate() http.HandlerFunc {
return
}
r.Body = http.MaxBytesReader(
w, r.Body, 1<<maxBodyShift,
)
// The body size cap is enforced by the MaxBodySize
// middleware, which runs before CSRF parses the form.
err = r.ParseForm()
if err != nil {
http.Error(
@@ -808,10 +798,8 @@ func (h *Handlers) processTargetCreate(
r *http.Request,
webhook database.Webhook,
) {
r.Body = http.MaxBytesReader(
w, r.Body, 1<<maxBodyShift,
)
// The body size cap is enforced by the MaxBodySize middleware,
// which runs before CSRF parses the form.
name := r.FormValue("name")
targetType := database.TargetType(r.FormValue("type"))
targetURL := r.FormValue("url")