Log the route pattern for redirected and rejected requests (closes #146)
All checks were successful
check / check (push) Successful in 2m57s

The access log wrote one INFO line per request carrying
r.URL.String(). Registered with Use, it runs ahead of the route
limiter, so a client flooding the unauthenticated receiver with
invented paths wrote attacker-chosen text of attacker-chosen length
into the operator's log, one line per request.

3xx and 4xx responses now log the chi route pattern in place of the
concrete URL, and the fixed literal "(unmatched)" when routing matched
nothing at all. One line per request is retained, so real traffic
stays observable and rate accounting still works, but the line's
content is now bounded by the service's own route table. 2xx and 5xx
keep the full URL.

The pattern is only populated after routing, so it is read in the
deferred part of the handler rather than before next.ServeHTTP.

No other access-log field changes.
This commit is contained in:
2026-08-17 20:41:53 +00:00
parent 2ee720a9af
commit fc115058ef
3 changed files with 353 additions and 3 deletions

View File

@@ -904,8 +904,24 @@ requests and has the rest of its aggregate budget rejected there, so
the aggregate limit is what bounds those `WARN` lines — to under ten
times `RECEIVER_RATE_LIMIT` per minute per client IP, 1080 at the
defaults, where before it there was no bound at all. The access log is
bounded by neither limit: every request is recorded once at `INFO` with
its full URL, served or rejected alike.
bounded by neither limit: every request is recorded once at `INFO`,
served or rejected alike.
What the access log does bound is the _content_ of those lines. A 3xx
or 4xx response logs the chi route pattern — `/webhook/{uuid}`,
`/user/{username}//`, or the literal `(unmatched)` when the request hit
no route at all — in place of the concrete URL. Those are the outcomes
an unauthenticated client can drive for free: 404 and 429 on any
invented receiver path, a login redirect on any invented profile path.
Logging the URL there would let a flood write text of its own choosing,
at a length of its own choosing, into the log. The pattern comes from
the service's own route table, so an operator sizing log storage can
multiply a fixed per-line cost by the request rate the rate limits
allow. 2xx and 5xx responses keep the full URL, query string included:
a success resolved against a static route or against the operator's own
data (on the receiver, against a stored entrypoint UUID), and a 5xx is
a bug in this service, where the exact URL is the evidence and no
client can provoke one at will.
Every limiter here — receiver, login, and password change — identifies
the client the same way, through one shared key function: the