Repository navigation
Description
Summary
A remote, unauthenticated client can make a default-configured HTTP/3 server allocate tens to thousands of times more memory than the client sends, or crash it outright, using a single small request. This is the QPACK amplification attack described in FoxIO's recent "Header Accordions" post, and h3 0.0.8 is listed among the affected implementations.
QPACK compresses HTTP/3 headers so that repeated or known fields cost a few bytes on the wire — a single byte (0xfa) references static-table entry 58, strict-transport-security: max-age=31536000; includesubdomains; preload, which decodes to a ~100-byte field (RFC 9114 §4.2.2 accounting). A tiny header block therefore expands into a large decoded Vec<HeaderField> and then a HeaderMap. Two gaps in h3 make that dangerous:
1. max_field_section_size is unlimited by default
Config::default() inherits Settings::default(), which sets max_field_section_size to VarInt::MAX (RFC 9114 §4.2.2 permits endpoints to impose a limit; the default does not). The incremental decoded-size enforcement added for #53 therefore never fires for a default-configured server. qpack::decode_stateless will happily decode a ~10 KB block of one-byte static-table references into hundreds of MB of decoded fields before any check can apply, at attacker-controlled CPU and memory cost.
The "unlimited" default in §7.2.4.1 only defines how a peer's advertised setting should be interpreted; nothing requires the local enforcement limit to be unlimited.
2. Header::try_from panics past http::HeaderMap's internal cap
Even with a limit configured, decoded field count is unbounded by a byte limit: RFC 9114 §4.2.2 charges each field ≥32 bytes overhead, so tiny fields exhaust a large byte limit slowly, but more than http::HeaderMap's internal MAX_SIZE (2^15 slots; not publicly exposed) still reach Header::try_from, where the conversion panics:
size overflows MAX_SIZE: MaxSizeReached (http map.rs `with_capacity`)
A ~1 MB header section of x-0: …x-32768: fields (which passes, e.g., a 4 MiB configured limit) crashes the connection/task with a remotely-triggered panic.
Proposed direction
h3 already has the incremental budget machinery; both gaps need closing so that oversized input is rejected early as a per-stream error:
- enforce a finite default
max_field_section_size(e.g. 64 KiB, in line with the ecosystem: Envoy 60 KiB, nginx 32 KiB), configurable via the existing builders, and - replace the panicking
HeaderMappopulation with fallible conversion so excess is surfaced as an error.
References
- Write-up: FoxIO, "Header Accordions: QPACK amplification attacks against HTTP/3 servers" — https://foxio.io/blog/header-accordions-qpack-amplification-attacks-against-http3-servers
- RFC 9114 §4.2.2 "Header Size Constraints" — https://www.rfc-editor.org/rfc/rfc9114.html#name-header-size-constraints
- RFC 9114 §7.2.4.1 "Defined Settings Parameters" — https://www.rfc-editor.org/rfc/rfc9114.html#name-defined-settings-parameters
- Related prior work: Make header decoding fail when MAX_FIELD_SECTION_SIZE is exceeded #53
- Fix: fix: mitigate QPACK header amplification attacks ("Header Accordions") #378
Activity
rmcwhorter commentedon Oct 22, 2026
@seanmonstar mind taking a look? it goes without saying that this is pretty important
thanks!
EDIT: feel free to make whatever changes you want to get it across the line; as long as the core functionality works, I don't care about style and nits
seanmonstar commentedon Oct 22, 2026
See also #377
rmcwhorter commentedon Oct 22, 2026
noted 👍
feat(http3): serve HTTP/3 over QUIC behind an `http3` feature