Harden http.Server: slowloris timeouts and form body limit (closes #92) #123
@@ -29,6 +29,14 @@ P1: implement blocked networks configuration to extend SSRF protection
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-09-21 http.Server hardening (closes #92): added
|
||||
`HTTPReadHeaderTimeout` (10s, bounds the slowloris header dribble) and
|
||||
`HTTPIdleTimeout` (120s, bounds keep-alive reuse) alongside the
|
||||
existing timeouts and wired them onto the server; added a `LimitBody`
|
||||
middleware capping the two form POST bodies (`POST /`, `POST /generate`)
|
||||
at `MaxFormBytes` (1 MiB) and returning 413, applied ahead of the CSRF
|
||||
middleware so an oversized body is refused as 413 rather than being read
|
||||
as a missing CSRF token (403); left `WriteTimeout` at 60s unchanged
|
||||
- 2026-08-07 update golangci-lint to v2.12.2 with the canonical
|
||||
`.golangci.yml` (v2 schema, `default: all` minus six disabled
|
||||
linters, `lll` 88, tests included): bumped the pinned
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// MaxFormBytes bounds the request body accepted on the HTML form POST
|
||||
// routes (POST / and POST /generate). The forms carry a handful of short
|
||||
// fields, so 1 MiB is generous while making the bound explicit rather than
|
||||
// resting on ParseForm's incidental 10 MB cap.
|
||||
const MaxFormBytes = 1 << 20 // 1 MiB
|
||||
|
||||
// LimitBody returns middleware that caps the request body on POST requests
|
||||
// at maxBytes and rejects an oversized body with 413 Request Entity Too
|
||||
// Large.
|
||||
//
|
||||
// It parses the form here, before the CSRF middleware reads the token from
|
||||
// it. The CSRF middleware reads the token with PostFormValue, which
|
||||
// swallows a parse error, so if the body were only capped there an
|
||||
// oversized body would read as a missing token and be refused as 403. By
|
||||
// parsing under the cap first, an oversized body is refused as 413. A
|
||||
// successful parse is cached on the request, so the CSRF check and the
|
||||
// handler reuse it rather than reading the body again.
|
||||
func (s *Handlers) LimitBody(maxBytes int64) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
|
||||
|
||||
err := r.ParseForm()
|
||||
|
||||
var tooLarge *http.MaxBytesError
|
||||
if errors.As(err, &tooLarge) {
|
||||
http.Error(w, "Request body too large",
|
||||
http.StatusRequestEntityTooLarge)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
"sneak.berlin/go/pixa/internal/handlers"
|
||||
"sneak.berlin/go/pixa/internal/static"
|
||||
)
|
||||
|
||||
@@ -46,8 +47,10 @@ func (s *Server) SetupRoutes() {
|
||||
|
||||
// Login/generator UI. The form routes carry CSRF protection; the
|
||||
// token cookie is independent of the session cookie, so it also
|
||||
// covers the login POST, where no session exists yet.
|
||||
// covers the login POST, where no session exists yet. LimitBody caps
|
||||
// the POST body ahead of CSRF, which reads its token from that body.
|
||||
s.router.Group(func(r chi.Router) {
|
||||
r.Use(s.h.LimitBody(handlers.MaxFormBytes))
|
||||
r.Use(s.h.CSRF())
|
||||
r.Get("/", s.h.HandleRoot())
|
||||
r.Post("/", s.h.HandleRoot())
|
||||
|
||||
Reference in New Issue
Block a user