Skip to content

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

Open

Description

@rmcwhorter

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 HeaderMap population with fallible conversion so excess is surfaced as an error.

References

Activity

  1. rmcwhorter commented on Oct 22, 2026

    @rmcwhorter
    Author

    @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

  2. seanmonstar commented on Oct 22, 2026

    @seanmonstar
    Member

    See also #377

  3. rmcwhorter commented on Oct 22, 2026

    @rmcwhorter
    Author

    3. See also #377

    noted 👍

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions