POST /api/v1/reports is unauthenticated, wildcard-CORS, and unrated-limited #20
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
The backend's only write endpoint accepts unauthenticated writes from any origin on the internet, with no rate limit. Verified on
mainatfbfe1df.backend/internal/server/routes.go:27-29:backend/internal/middleware/middleware.go:113-129:Composition of the three:
HandleReportperforms no identity check.report.ClientIDis a client-supplied string with no verification — anyone can claim anyclientId.*"only for public, unauthenticated read-only APIs."POST /api/v1/reportsis neither read-only nor, ideally, unauthenticated.AllowedMethodsalso advertisesPUTandDELETE, which no route implements.backend/internal/handlers/report.go:71->s.buf.Append(rpt)), which is backed by disk. An unauthenticated attacker can drive unbounded disk growth at 1 MiB per request (maxReportBodyBytes), which is a remote denial-of-service and a storage-cost attack.What is already correct and should not be regressed:
http.MaxBytesReaderis applied (report.go:39-41), the decode-error path returns a generic{"status":"error"}with no internal detail leaked (report.go:50-56), andAllowCredentialsisfalse.This needs a decision before implementation
The right answer depends on how NetWatch is meant to be deployed, which is not recorded anywhere in the repo. Three viable postures:
Authorization: Bearer <token>; the token is baked into the frontend build. Stops casual abuse, but the token is public by construction since it ships in a browser bundle. CORS narrows to the known frontend origin(s).Recommendation: (b), with CORS narrowed. The frontend is a static SPA served to arbitrary browsers, so any credential it carries is public — (a) buys the appearance of auth without the substance. (c) is the most secure but forecloses the hosted use case the frontend's design implies. (b) defends the real threat (unbounded disk growth) without pretending the endpoint is authenticated.
Under (b) the concrete requirements would be: per-IP token-bucket rate limiting on
POST /api/v1/reportskeyed off the trusted-proxy-resolved client IP from #19; a configurable absolute cap on total buffered report bytes on disk, past which writes are rejected with429/507rather than growing without bound; andAllowedOriginsnarrowed to a configurable allowlist withAllowedMethodsreduced toGET, POST, OPTIONS.@sneak — please pick a posture, or confirm (b). Assigning to you for that decision. Implementation is blocked until it is made; the definition of done below is written for (b) and should be amended if you choose otherwise.
Definition of done (assuming posture (b))
AllowedOriginsis a configurable allowlist (env var via the existingviperconfig), not*. Document the variable inbackend/README.md.AllowedMethodsis reduced to the methods actually served:GET,POST,OPTIONS.POST /api/v1/reports, using the trusted-proxy-resolved client IP. Limits are configurable with sane defaults.internal/reportbuf, not only at the handler.cd backend && make checkpasses; rootmake checkpasses.backend/README.mddocuments every new environment variable.TODO.mdupdated in the same commit.(closes #N).Implementation requirements
GO_HTTP_SERVER_CONVENTIONS.mdfor config (viper+AutomaticEnv+SetDefault), middleware shape, and fx wiring.RemoteAddrbehind a proxy would rate-limit the proxy, not the client. Sequence this after #19.golang.org/x/time/rateis stdlib-adjacent and the right default. Consult the Go package defaults before adding anything else.