Failing test first for the per-route body limit. The key regression: an oversized POST / or POST /generate carrying an otherwise valid CSRF cookie and token must return 413. CSRF landed on next and reads its token from the form body, so if the limit ran after CSRF the truncated body would read as a missing token (403) and if it ran after the handler a valid token would succeed (303). 413 proves the limit fires first. Two within-limit tests guard against the limit disturbing a normal request. Model: opus-4-8
178 lines
4.8 KiB
Go
178 lines
4.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"sneak.berlin/go/pixa/internal/config"
|
|
"sneak.berlin/go/pixa/internal/encurl"
|
|
"sneak.berlin/go/pixa/internal/session"
|
|
)
|
|
|
|
// Form field names and a throwaway source image URL for the body-limit
|
|
// tests.
|
|
const (
|
|
sourceURLField = "url"
|
|
testSourceURL = "https://example.com/a.jpg"
|
|
)
|
|
|
|
// newBodyLimitTestRouter mirrors the production wiring for the form POST
|
|
// routes (see server.SetupRoutes): LimitBody sits in front of the CSRF
|
|
// middleware, which sits in front of the handlers. maxBytes is the body
|
|
// cap under test, so a test can trip the limit with a small body.
|
|
func newBodyLimitTestRouter(
|
|
t *testing.T, maxBytes int64,
|
|
) (*Handlers, http.Handler) {
|
|
t.Helper()
|
|
|
|
cfg := &config.Config{SigningKey: testSigningKey, Debug: true}
|
|
|
|
sessMgr, err := session.NewManager(testSigningKey)
|
|
if err != nil {
|
|
t.Fatalf("session.NewManager() error = %v", err)
|
|
}
|
|
|
|
encGen, err := encurl.NewGenerator(testSigningKey)
|
|
if err != nil {
|
|
t.Fatalf("encurl.NewGenerator() error = %v", err)
|
|
}
|
|
|
|
protect, err := newCSRFProtect(testSigningKey, cfg.Debug)
|
|
if err != nil {
|
|
t.Fatalf("newCSRFProtect() error = %v", err)
|
|
}
|
|
|
|
h := &Handlers{
|
|
log: slog.New(slog.DiscardHandler),
|
|
config: cfg,
|
|
sessMgr: sessMgr,
|
|
encGen: encGen,
|
|
csrfProtect: protect,
|
|
}
|
|
|
|
r := chi.NewRouter()
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(h.LimitBody(maxBytes))
|
|
r.Use(h.CSRF())
|
|
r.Get("/", h.HandleRoot())
|
|
r.Post("/", h.HandleRoot())
|
|
r.Post("/generate", h.HandleGenerateURL())
|
|
})
|
|
|
|
return h, r
|
|
}
|
|
|
|
// TestOversizedLoginPostRejectedBeforeCSRF is the core regression: an
|
|
// oversized POST / carrying an otherwise valid CSRF cookie and token must
|
|
// be rejected with 413. If the body limit ran after CSRF, the truncated
|
|
// body would read as a missing token and return 403; if it ran after the
|
|
// handler, a valid token would return 303. Getting 413 proves the limit
|
|
// fires before CSRF parses the form.
|
|
func TestOversizedLoginPostRejectedBeforeCSRF(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, srv := newBodyLimitTestRouter(t, 16)
|
|
|
|
cookies, token := csrfCredentials(t, srv, nil)
|
|
|
|
rec := postForm(srv, "/", cookies, url.Values{
|
|
loginKeyField: {testSigningKey},
|
|
csrfTokenField: {token},
|
|
})
|
|
|
|
if rec.Code != http.StatusRequestEntityTooLarge {
|
|
t.Errorf("oversized POST / status = %d, want %d",
|
|
rec.Code, http.StatusRequestEntityTooLarge)
|
|
}
|
|
}
|
|
|
|
// TestOversizedGeneratePostRejectedBeforeCSRF is the same regression for
|
|
// POST /generate, which also parses a form behind CSRF.
|
|
func TestOversizedGeneratePostRejectedBeforeCSRF(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h, srv := newBodyLimitTestRouter(t, 16)
|
|
|
|
sessionCookie := newSessionCookie(t, h)
|
|
|
|
cookies, token := csrfCredentials(t, srv, []*http.Cookie{sessionCookie})
|
|
cookies = append(cookies, sessionCookie)
|
|
|
|
rec := postForm(srv, "/generate", cookies, url.Values{
|
|
sourceURLField: {testSourceURL},
|
|
csrfTokenField: {token},
|
|
})
|
|
|
|
if rec.Code != http.StatusRequestEntityTooLarge {
|
|
t.Errorf("oversized POST /generate status = %d, want %d",
|
|
rec.Code, http.StatusRequestEntityTooLarge)
|
|
}
|
|
}
|
|
|
|
// TestWithinLimitLoginPostSucceeds verifies the limit does not disturb a
|
|
// normal request: under the production cap, a valid login still parses and
|
|
// establishes a session (303). This guards against the body limit
|
|
// consuming or corrupting the form the CSRF check and handler depend on.
|
|
func TestWithinLimitLoginPostSucceeds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, srv := newBodyLimitTestRouter(t, MaxFormBytes)
|
|
|
|
cookies, token := csrfCredentials(t, srv, nil)
|
|
|
|
rec := postForm(srv, "/", cookies, url.Values{
|
|
loginKeyField: {testSigningKey},
|
|
csrfTokenField: {token},
|
|
})
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("within-limit POST / status = %d, want %d",
|
|
rec.Code, http.StatusSeeOther)
|
|
}
|
|
|
|
var authed bool
|
|
|
|
for _, c := range rec.Result().Cookies() {
|
|
if c.Name == session.CookieName && c.Value != "" {
|
|
authed = true
|
|
}
|
|
}
|
|
|
|
if !authed {
|
|
t.Error("within-limit valid login did not set a session cookie")
|
|
}
|
|
}
|
|
|
|
// TestWithinLimitGeneratePostSucceeds is the same non-regression check for
|
|
// POST /generate.
|
|
func TestWithinLimitGeneratePostSucceeds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h, srv := newBodyLimitTestRouter(t, MaxFormBytes)
|
|
|
|
sessionCookie := newSessionCookie(t, h)
|
|
|
|
cookies, token := csrfCredentials(t, srv, []*http.Cookie{sessionCookie})
|
|
cookies = append(cookies, sessionCookie)
|
|
|
|
rec := postForm(srv, "/generate", cookies, url.Values{
|
|
sourceURLField: {testSourceURL},
|
|
"format": {"jpeg"},
|
|
csrfTokenField: {token},
|
|
})
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("within-limit POST /generate status = %d, want %d",
|
|
rec.Code, http.StatusOK)
|
|
}
|
|
|
|
if !strings.Contains(rec.Body.String(), "/v1/e/") {
|
|
t.Error("within-limit generate response did not contain a generated URL")
|
|
}
|
|
}
|