mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-16 16:04:30 -04:00
The map response reader now caps a single message at 256 MiB on the wire and 1 GiB after zstd decompression. The server-chosen uint32 size prefix previously let a malicious control server make us allocate up to 4 GiB before reading any body bytes, and the decoded size was unbounded, so a small zstd frame could expand into gigabytes of JSON. A 16 MB cap has been hit by real production traffic before, so both limits sit far above plausible legitimate sizes. The size-prefixed read moved into a readMapResponseMessage helper, and the newer control/tsp path already enforced both kinds of bounds; this brings the long-poll path it replaces in line, with more generous limits for large tailnets. ts2021.Client.Do additionally caps every noise response body with httpbody.LimitSize, so a malicious or buggy control server can't make us buffer an unbounded response. Client.Do shadows the embedded http.Client's Do method, so register, set-dns, set-device-attr, audit-log, all DoNoiseRequest consumers (webclient, tailnet lock, SSH actions, id-token, feature queries), and the debug CLI get the cap without per-call-site changes, and future noise endpoints get it for free. The cap lives in the new util/httpbody package so other HTTP clients can adopt the same convention: LimitSize looks the size limit up from res.Request's context (a Response knows the Request that produced it), falling back to DefaultMaxSize, 1 MiB, when the context carries no override. It is like io.LimitReader except that reads past the limit fail with an error wrapping httpbody.ErrTooLarge instead of silently truncating, and a body of at most the limit, including one of exactly the limit, reads back without error: the wrapper probes for EOF once the limit is exhausted to tell an exactly-at-limit body from an oversize one. The per-request override, httpbody.WithMaxSize, is a context key, so transports pick it up with no API changes; LimitSizeTo applies an explicit limit ignoring any override. Repeated LimitSize or LimitSizeTo calls replace the previous limit rather than compounding it, so a later call can raise or remove the limit an earlier one set. Responses that stream an unbounded number of individually bounded messages disable the cap with httpbody.WithMaxSize(ctx, 0): the /machine/map long-poll and control/tsp's map session, whose messages are already capped per-message (by readMapResponseMessage and decodeMsg, and by tsp's framedReader and boundedReader). Their non-200 error bodies are not message streams, so those are capped with LimitSizeTo instead. The tailnet lock /tka/init/begin, /tka/sync/offer and /tka/affected-sigs responses can carry per-node key signatures or missing AUMs, which at 100,000 peers reach tens of MB, so they raise the cap to 512 MiB. The per-response io.LimitedReader decoders that silently truncated those responses at 1 or 10 MiB are removed: the transport cap is now the single enforcement point, and it reports oversize bodies instead of truncating them. The /key fetch over plain TLS switches from io.LimitReader to httpbody.LimitSizeTo, so an oversized response reports the problem instead of producing a confusing truncated-JSON error. Thanks to Ben Carman for the report! Updates tailscale/corp#48187 Reported-by: Ben Carman Change-Id: Ibf95e1ab9e4f0d7ef8866e8c26e62ed2a514455a Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>