Report ingest correctness: returns 200 on storage failure, 400 on oversize, logs untrusted body #23
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Four defects in
POST /api/v1/reportsand the surrounding handler layer. Verified onmainatfbfe1df.1. Returns
200 {"status":"ok"}when the report was NOT storedbackend/internal/handlers/report.go:70-80:The error is logged and then discarded. The client is told its report was accepted when it was dropped. Any client-side retry logic is defeated, and the failure is invisible to everything except whoever is reading server logs.
The same pattern exists on the shutdown path:
internal/reportbuf/reportbuf.go:78-83'sOnStopignores the result of the final flush, andwriteFile(reportbuf.go:161) only logs. A failed final flush loses data silently and the process still exits0. (The lifecycle half of that is tracked in #22; the error-propagation half belongs here.)2. Oversize bodies return
400, not413report.go:39-56:http.MaxBytesReaderis correctly applied, but the decode error path never distinguishes a*http.MaxBytesErrorfrom malformed JSON. Both produce400 Bad Request. A client that sent a valid but too-large report gets told its JSON is broken.CODE_STYLEGUIDE_GO.mdshows the intendedhttp.StatusRequestEntityTooLargebehaviour.3. Body size limit is per-route, not global
Policy requires a maximum request body size "enforced on all endpoints".
MaxBytesReaderappears only inreport.go. There is no body-limit middleware ininternal/middleware/middleware.goand none registered inroutes.go. The healthcheck route and every future route accept unbounded input.4. Raw untrusted request body is logged at Info level
report.go:62-68logs"geo", string(rpt.Geo)— the raw, unvalidated, attacker-controlledjson.RawMessagedeclared atreport.go:25, up to the full 1 MiB body cap, on every request.client_idandtimestampare logged verbatim too.This is a log-injection and log-volume amplification vector: an attacker controls both the content and the size of what lands in the log pipeline, at 1 MiB per request, with no authentication (#20).
Also in scope — trivial, same files
decodeJSONhelper.GO_HTTP_SERVER_CONVENTIONS.mddefinesdecodeJSONalongsiderespondJSONas a base handler helper.handlers.go:59-73defines onlyrespondJSON;report.go:44decodes inline.chi middleware.Recovererwrites panic stack traces to stderr as unstructured plain text, bypassing slog. The client correctly gets a bare 500 with no body — that part is fine — but the stack should go through the structured logger.Not defects, do not regress them: the client-facing error body is already clean (
{"status":"error"}, no internals leaked), andContent-Typeis correctly set beforeWriteHeaderinhandlers.go:65-66.Definition of done
413; malformed JSON returns400. Distinguish viaerrors.Ason*http.MaxBytesError.geoblob is no longer logged. If geo data must be observable, log a bounded, validated projection of it (for example a length, or specific parsed fields), never the raw attacker-controlled bytes. Apply the same reasoning toclient_id: bound its logged length.decodeJSONhelper added tohandlers.goand used byreport.go.Recovereris routed through slog as structured JSON rather than unstructured stderr text.httptest.cd backend && make checkpasses; rootmake checkpasses.TODO.mdupdated in the same commit.(closes #N).Implementation requirements
GO_HTTP_SERVER_CONVENTIONS.mdfor the middleware and handler-helper shapes, andCODE_STYLEGUIDE_GO.mdfor error handling.maketargets only; never rawgoinvocations.