Repository navigation
fix: mitigate QPACK header amplification attacks ("Header Accordions") - #378
Open
rmcwhorter wants to merge 1 commit into
Open
rmcwhorter wants to merge 1 commit into
rmcwhorter wants to merge 1 commit into
Conversation
rmcwhorter
force-pushed
the
fix/qpack-header-amplification
branch
from
October 22, 2026 02:49
20f02db to
d3e6271
Compare
rmcwhorter
force-pushed
the
fix/qpack-header-amplification
branch
from
October 22, 2026 02:55
d3e6271 to
2507820
Compare
rmcwhorter
commented
Oct 22, 2026
rmcwhorter
commented
Oct 22, 2026
rmcwhorter
commented
Oct 22, 2026
rmcwhorter
force-pushed
the
fix/qpack-header-amplification
branch
from
October 22, 2026 03:08
2507820 to
d2c7254
Compare
- Default max_field_section_size is now 64 KiB (65536 bytes), enforced incrementally while decoding; oversized requests are answered with HTTP 431 and fail with StreamError::HeaderTooBig. The RFC 9114 section 7.2.4.1 "unlimited" default is retained for parsing the peer's advertised setting. - Header conversion uses HeaderMap::try_with_capacity/try_append and returns the new HeaderError::ExcessiveHeaders instead of panicking when HeaderMap's internal capacity is exceeded, reported as an H3_MESSAGE_ERROR stream error. - Bump http dependency requirement to 1.1 for try_append/try_with_capacity.
rmcwhorter
force-pushed
the
fix/qpack-header-amplification
branch
from
October 22, 2026 03:13
d2c7254 to
7ab9d00
Compare
This was referenced Oct 24, 2026
lumielis
pushed a commit
to sailantis/volucer
that referenced
this pull request
Oct 28, 2026
…imit The ceiling I shipped was 1 MiB and it was wrong -- it admitted the exact crash it existed to prevent. `h3` 0.0.8 builds the request's header map with `HeaderMap::with_capacity(fields.len())`, and that call **panics** above `http`'s limit. Measured rather than inferred: `with_capacity(24_576)` succeeds and `with_capacity(24_577)` aborts with `size overflows MAX_SIZE`. (`MAX_SIZE` is 1<<15 *raw* slots and `to_raw_capacity(n)` is `n + n/3`, so the logical limit is 24 576, not 32 768. An earlier comment in this file said 32 768 and was wrong twice over.) The budget bounds the field *count*, because RFC 9114 §4.2.2 charges each field at least `QpackField::ESTIMATED_OVERHEAD_BYTES` (32) bytes of it. So a 1 MiB ceiling admitted 32 768 fields -- 8192 past the panic. An attacker sending 24 577 fields at 33 bytes each needed 810 741 bytes: under the ceiling, so it passed validation and then panicked the worker in `Header::try_from`. The ceiling is now 512 KiB, which caps the worst case at 16 384 fields -- a third below the threshold. `HEADER_MAP_MAX_FIELDS` names the measured limit, and `the_ceiling_cannot_admit_a_panicking_field_count` asserts the arithmetic between two constants that live in different crates, so an `http` upgrade that lowers the limit fails a test rather than a deployment. Reverting the ceiling to 1 MiB makes it fail with both numbers in the message. Also records why the fork carries nothing, checked rather than recalled: hyperium/h3#378 is still open and unmerged, and h3 0.0.8 is still the newest release -- so there is no fix to carry, and a `[patch]` at a fork with no diff would be the false statement this project's policy forbids. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #379
Summary
Mitigates the QPACK header amplification attacks against HTTP/3 servers described in "Header Accordions" (FoxIO, Oct 20 2026), which lists
h3as vulnerable: a small QPACK-encodedHEADERSblock can expand into a very large decoded header section, and in h3's case triggers a thread panic that kills the connection:The blog's timeline notes this was reported to h3 maintainers on Dec 4 2025 and the issue was closed with the intent to change the default setting. This PR implements that.
Root causes
max_field_section_sizedefaulted toVarInt::MAX, i.e. effectively unlimited, so the existing incremental decoded-size enforcement inqpack::decode_statelessnever triggered under default configuration.Header::try_frompanicked viaHeaderMap::with_capacity/append(expect("size overflows MAX_SIZE")in thehttpcrate once the map's internal capacity of 32,768 entries is exceeded), taking down the whole connection.Changes
h3/src/config.rs: introduceDEFAULT_MAX_FIELD_SECTION_SIZE(64 KiB) and use it inConfig::default(). Decoded header sections are rejected incrementally while decoding; oversized requests are answered with431and fail withStreamError::HeaderTooBig(existing machinery, now reachable by default). Per RFC 9114 §7.2.4.1, the default value ofSETTINGS_MAX_FIELD_SECTION_SIZEremains unlimited when parsing the peer's advertised setting, so outgoing-size checks behave exactly as before.h3/src/proto/headers.rs: convert withHeaderMap::try_with_capacity(...).unwrap_or_default()andtry_appendinstead of the panickingwith_capacity/append. Overflow now surfaces as the newHeaderError::ExcessiveHeadersvariant, which the server reports as a stream error of typeH3_MESSAGE_ERRORinstead of a panic.h3/Cargo.toml: raisehttprequirement to1.1(the first release withHeaderMap::try_append/try_with_capacity; verified absent in1.0.0).changelog-h3.md: security entry under Unreleased.Compatibility notes
proto::headers::HeaderErrorgains theExcessiveHeadersvariant (reachable only whenmax_field_section_sizeis configured above whatHeaderMapcan represent).client::Builder::max_field_section_size()/server::Builder::max_field_section_size()can raise the limit.Testing
Four new tests, previously failing on
master(two via the exact panic above, one proving 100k one-byte static-table references decode without error under the old default):qpack::decoder::tests::default_limit_rejects_header_amplification_incrementally— decoding aborts withHeaderTooLongbefore consuming the block.proto::headers::tests::oversized_qpack_field_section_returns_error—Header::try_fromreturnsErrinstead of panicking.tests::request::{default_limit_rejects_oversized_request_without_closing_connection, large_limit_rejects_header_map_overflow_without_closing_connection}— end-to-end over real QUIC: an oversized request (bypassing the client-side limit) is rejected withHeaderTooBig(default config) /H3_MESSAGE_ERROR(4 MiB configured limit), and the connection continues serving subsequent requests.Validation
cargo test --workspace(240 tests inh3, plus h3-quinn, h3-datagram, h3-webtransport and examples)cargo fmt --all -- --checkcargo clippy --all-targets --all-features(no new warnings)cargo doc -p h3 --no-deps(no warnings)duvet report— no snapshot change (the newtype=testcitations target requirements already covered asimplementation,test)Related work