fix(api): cap ingest request body and decompressed gzip size#287
Conversation
handleIngestRun read the full request body with io.ReadAll and the gzip middleware decompressed it with no limit, so a small gzip payload could inflate to an arbitrary size before any validation ran. A test payload under 1MB pushed the process past 1.6GB of RSS. The raw body is now capped at 10MiB regardless of encoding, and a gzip-encoded body's decompressed output is separately capped at 50MiB, both via http.MaxBytesReader. Either limit being exceeded now returns 413 instead of buffering the full payload first.
Review follow-up: the ingest caps left every other body-reading endpoint unbounded, including the one endpoint that needs it most. POST /auth/login decodes its body with json.Decoder and no limit, and it is reachable before any authentication runs. Rate limiting doesn't cover it either - rate_limit.enabled defaults to false. Measured on this branch: a 64MiB body shaped as one long username costs ~320MiB of allocation, a larger amplification factor than the ingest path this PR set out to fix, and it needs no credentials at all. Same shape at the API key handler and the five admin decode sites. Adds limitRequestBody(n) and mounts it on the /auth and /admin route groups at 1 MiB. A body advertising a Content-Length over the cap is refused on the headers without being read; a chunked body is bounded by the reader and surfaces to the handler as a decode error. Applied per route group rather than globally on purpose: a global wrapper would become the underlying reader for gzipRequestBody's own MaxBytesReader, and the smaller limit would silently win on ingest. On /admin the cap is mounted ahead of requireAuth so an unauthenticated flood is refused on size rather than read in full and then rejected as unauthorized. Tests cover the middleware directly (Content-Length rejection, chunked bounding, the handler never reading past the cap, normal bodies passing through) and the router wiring, driven through the real buildRouter with no store configured so a missing limiter reaches a nil-store panic instead of a 413. Verified the wiring test fails when the middleware is removed from a route group.
|
Reviewed this and pushed b0ab161. The core mechanism here is sound — I probed it directly rather than reasoning about it, and confirmed a gzip stream whose compressed size exceeds 10 MiB does surface as 413 (the What I added is the endpoint the caps didn't reach.
|
What
The ingest endpoint read the full request body with io.ReadAll and decompressed gzip bodies with no size limit, so a small gzip payload could inflate to an arbitrary size in memory before any validation ran. Verified live: an 815KB request pushed the API process from a 40MB baseline to 1.67GB RSS in under a second.
How
Test plan
go test -race ./pkg/api/...passes,go vetandgofmtclean.