Files
webhooker/internal
sneak 74a57ad237
All checks were successful
check / check (push) Successful in 3m0s
Bound the access log line against client-chosen text (closes #146)
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. The pattern
is only populated after routing, so it is read in the deferred part of
the handler rather than before next.ServeHTTP.

The route pattern alone does not close the hole, because it leaves two
other ways for a request to choose the size of the line it writes.

The query string is one: /.well-known/healthcheck and /s/* answer 200
to anyone with no rate limiter in front of them, and /pages/login
behind only the login limiter, so appending 8 KB after the '?' bought
the same amplification as an invented 404 path. The branches that keep
the concrete URL now log the path only, with the query replaced by the
fixed marker "?(redacted)". Nothing debuggable is lost: `page`, on the
authenticated pagination links, is the only query parameter this
service reads.

The headers are the other: useragent and referer are logged on every
line, including the correctly redacted ones, so an 8 KB User-Agent
plus an 8 KB Referer produced a 24 KB line whose url field read
"(unmatched)". Each field a client supplies is now truncated rather
than dropped -- a truncated User-Agent is still worth reading -- to
512 bytes for url, useragent and referer, 128 for request_id (chi
passes an inbound X-Request-Id header straight through), and 32 for
method, which Go accepts as any token up to the header size limit.
Truncation also drops invalid UTF-8, which an encoder would otherwise
expand six-fold past the budget.

Each budget is spent in encoded bytes rather than in the bytes the
client sent, because the line an operator stores is the encoded one.
Every rune is charged what the wider of the two handlers
internal/logger can select will emit for it: two bytes for a quotation
mark, a backslash or a tab; six for a non-printable rune below
U+10000; and ten for one at or above it, which the text handler's
strconv.Quote spells \UXXXXXXXX where the JSON handler passes the four
UTF-8 bytes through untouched. Go's header parser accepts every one of
them in a header value -- U+1000C is four bytes all >= 0x80, which
httpguts.ValidHeaderFieldValue accepts and net/textproto does not
strip -- so counted raw a 512-byte budget bought a 1,024-byte field,
and charging the astral case six rather than ten bought an 861-byte
one on a tty. Plain ASCII still encodes one byte for one, so a real
browser's User-Agent fits whole, while a value built out of escapes
keeps a proportionally shorter prefix.

A complete line is now at most 2,560 bytes under either handler:
3*(512+11) for url, useragent and referer, 128+11 for request_id,
32+11 for method and a fixed portion of 336 for JSON come to 2,087,
and 2,037 for the text handler's 286-byte fixed portion, both stated
with headroom. The tests assert it against 8 KB in the path, in the
query and in each of the three headers, including values built from
the characters the handlers escape and from an unassigned astral code
point, and against a 5xx whose concrete url is at its own budget on
the same line. Every case runs through both handler shapes, since the
two do not escape alike and the number is quoted unqualified. The
README states it so an operator can size log storage against it.
2026-08-17 22:12:58 +00:00
..