Skip to content

fix: mitigate QPACK header amplification attacks ("Header Accordions") - #378

Open
rmcwhorter wants to merge 1 commit into
hyperium:masterfrom
rmcwhorter:fix/qpack-header-amplification
Open

rmcwhorter wants to merge 1 commit into
hyperium:masterfrom
rmcwhorter:fix/qpack-header-amplification

Conversation

@rmcwhorter

@rmcwhorter rmcwhorter commented Oct 22, 2026 •

Copy link
Copy Markdown

Fixes #379

Summary

Mitigates the QPACK header amplification attacks against HTTP/3 servers described in "Header Accordions" (FoxIO, Oct 20 2026), which lists h3 as vulnerable: a small QPACK-encoded HEADERS block can expand into a very large decoded header section, and in h3's case triggers a thread panic that kills the connection:

thread 'tokio-runtime-worker' panicked at .../http-1.5.0/src/header/map.rs:536:43:
size overflows MAX_SIZE: MaxSizeReached

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

  1. max_field_section_size defaulted to VarInt::MAX, i.e. effectively unlimited, so the existing incremental decoded-size enforcement in qpack::decode_stateless never triggered under default configuration.
  2. Header::try_from panicked via HeaderMap::with_capacity/append (expect("size overflows MAX_SIZE") in the http crate once the map's internal capacity of 32,768 entries is exceeded), taking down the whole connection.

Changes

  • h3/src/config.rs: introduce DEFAULT_MAX_FIELD_SECTION_SIZE (64 KiB) and use it in Config::default(). Decoded header sections are rejected incrementally while decoding; oversized requests are answered with 431 and fail with StreamError::HeaderTooBig (existing machinery, now reachable by default). Per RFC 9114 §7.2.4.1, the default value of SETTINGS_MAX_FIELD_SECTION_SIZE remains unlimited when parsing the peer's advertised setting, so outgoing-size checks behave exactly as before.
  • h3/src/proto/headers.rs: convert with HeaderMap::try_with_capacity(...).unwrap_or_default() and try_append instead of the panicking with_capacity/append. Overflow now surfaces as the new HeaderError::ExcessiveHeaders variant, which the server reports as a stream error of type H3_MESSAGE_ERROR instead of a panic.
  • h3/Cargo.toml: raise http requirement to 1.1 (the first release with HeaderMap::try_append/try_with_capacity; verified absent in 1.0.0).
  • changelog-h3.md: security entry under Unreleased.

Compatibility notes

  • proto::headers::HeaderError gains the ExcessiveHeaders variant (reachable only when max_field_section_size is configured above what HeaderMap can represent).
  • Endpoints that send header sections larger than 64 KiB (decoded size) will now be rejected by default; 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 with HeaderTooLong before consuming the block.
  • proto::headers::tests::oversized_qpack_field_section_returns_error — Header::try_from returns Err instead 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 with HeaderTooBig (default config) / H3_MESSAGE_ERROR (4 MiB configured limit), and the connection continues serving subsequent requests.

Validation

  • cargo test --workspace (240 tests in h3, plus h3-quinn, h3-datagram, h3-webtransport and examples)
  • cargo fmt --all -- --check
  • cargo clippy --all-targets --all-features (no new warnings)
  • cargo doc -p h3 --no-deps (no warnings)
  • duvet report — no snapshot change (the new type=test citations target requirements already covered as implementation,test)

Related work

@rmcwhorter
rmcwhorter force-pushed the fix/qpack-header-amplification branch from 20f02db to d3e6271 Compare October 22, 2026 02:49
@rmcwhorter rmcwhorter changed the title Mitigate QPACK header amplification attacks ("Header Accordions") fix: mitigate QPACK header amplification attacks ("Header Accordions") Oct 22, 2026
@rmcwhorter
rmcwhorter force-pushed the fix/qpack-header-amplification branch from d3e6271 to 2507820 Compare October 22, 2026 02:55
Comment thread h3/src/proto/headers.rs Resolved
Comment thread h3/src/qpack/decoder.rs Resolved
Comment thread h3/src/qpack/decoder.rs Resolved
@rmcwhorter
rmcwhorter force-pushed the fix/qpack-header-amplification branch from 2507820 to d2c7254 Compare October 22, 2026 03:08
- 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
rmcwhorter force-pushed the fix/qpack-header-amplification branch from d2c7254 to 7ab9d00 Compare October 22, 2026 03:13
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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

QPACK header amplification: default-configured servers can be OOM'd or crashed by a small request ("Header Accordions")

1 participant