Tech10 min read

QPACK Header Accordions Exhaust Memory in 7 of 10 Tested HTTP/3 Implementations

IkesanContents

TL;DR

What happened October 20, 2026: FoxIO published QPACK amplification attacks against HTTP/3. One unauthenticated client connection can make a server run out of memory or keep its CPU at 100%. 7 of the 10 tested implementations are affected
Fixed in quic-go 0.57.0, Cloudflare quiche 0.29.3, lsquic 4.9.0 (OpenLiteSpeed 1.9.2+), Wireshark 4.6.2 / 4.4.12
Still unpatched as of October 20, 2026 Bun 1.4.3, hyperium/h3, aioquic / mitmproxy
What to do

  1. Update quic-go, quiche, lsquic/OpenLiteSpeed and Wireshark to the versions above
  2. Bun: do not expose HTTP/3 servers
  3. mitmproxy: disable HTTP/3 with --set http3=false
  4. h3 crate: set a reasonable max_field_section_size

On October 20, 2026, FoxIO published a set of amplification attacks that abuse QPACK, the header compression used by HTTP/3 (Header Accordions: QPACK Amplification Attacks Against HTTP/3 Servers).
The author, Sébastien Féry, tested 10 HTTP/3 implementations and got 7 of them to allocate far more memory than the request took on the wire.
It is a denial-of-service (DoS) attack: a small request makes the server burn through memory or CPU until it stops responding.

In every affected implementation, the decoded header size either had no limit, was counted differently from the spec, or was only checked after everything had been decoded.
The reports went out over almost a year, and some projects have shipped fixes while others have not.

QPACK and the header size limit

HTTP/3 is HTTP running over QUIC on UDP, and request and response headers are compressed with QPACK (RFC 9204).
QPACK has a static table, where common headers are pre-assigned numbers, and a dynamic table, which the sender fills per connection. The receiver turns each number back into the header’s name and value.

For example, the 71-byte strict-transport-security header can be sent as a single byte, 0xFA, which points to entry 58 of the static table.
The dynamic table is only available when the receiver allows it with SETTINGS_QPACK_MAX_TABLE_CAPACITY (the default is 0).
When it is allowed, the sender inserts entries over the encoder stream (a dedicated stream for updating the table).
HEADERS frames (the frames that carry headers) then refer to an entry with a 1-byte reference.
On the receiving side, far more data gets decoded than went over the wire.

The HTTP/3 spec (RFC 9114 Section 4.2.2) says the size of a header section is the decoded length of each name and value plus 32 bytes of overhead per field.
A header with a 1-byte name and a 1-byte value counts as 1 + 1 + 32 = 34 bytes.
Setting a limit is optional for an implementation, and a limit it does set can be advertised to the peer with SETTINGS_MAX_FIELD_SECTION_SIZE.
A server can answer a request over the limit with HTTP 431 (Request Header Fields Too Large).

Header Accordion and dynamic table references

The “Header Accordion” attack that gives the post its title fills one HEADERS frame with as many distinct header names as possible: a to z, then aa to zz, then three- and four-character names (te is skipped because it is treated specially).
A 1,000,000-byte HEADERS frame holds about 160,000 headers.
An implementation with no limit on decoded size decodes and keeps all 160,000 of them.

The other attack uses the dynamic table and sends huge numbers of 1-byte references.

flowchart TD
    A["Insert one ~4 KiB entry<br/>over the encoder stream"] --> B["Reference that entry<br/>with 1 byte in HEADERS"]
    B --> C["Repeat the reference<br/>thousands of times"]
    C --> D["Receiver allocates and copies<br/>4 KiB for every reference"]
    D --> E["About 4000x more memory<br/>than bytes on the wire"]

In implementations that allocate and copy the whole entry for every reference, each 1-byte reference turns into about 4 KiB of memory.
FoxIO measured a 4064:1 ratio against lsquic and Bun.

Five ways implementations got it wrong

MistakeWhat happens
Checking only the compressed frame sizeA small frame can expand into a huge header section without being stopped
Decoding everything before checkingThe memory is already allocated by the time the check runs
Leaving out the 32-byte overheadLots of 1-byte headers fit dozens of times over the limit
Defaulting to no limit when unconfiguredNothing is enforced with the default settings
Never enforcing a configured limitThe limit is advertised to the peer but ignored on receive

The fix FoxIO recommends is to keep a running total during decoding, including the 32-byte per-field overhead, and stop as soon as it goes over the limit.

Results by implementation

ImplementationCVEImpact reported by FoxIOCause
quic-goCVE-2025-64702~15x retained heapOnly checks the compressed size
Cloudflare quicheCVE-2026-12523~20xMissing 32-byte overhead, preallocation from declared frame length
lsquicNone (fixed)~600-1800xNo cumulative decoded limit, unlimited HEADERS blocks
BunNone~4000xBuilt on lsquic but removes lsquic’s cumulative cap
aioquic / mitmproxyNone~20-30xLimit is parsed but never enforced
hyperium/h3None (reported, not fixed)Thread panicHeaderMap::append panics at 32,768 entries
WiresharkCVE-2025-13945CrashHTTP/3 dissector does not bound decoded headers
Google quiche—Not affectedEnforces its cap during decoding
IIS (http.sys)—Not affectedChecks incrementally while decoding, 4K QPACK table
ngtcp2 / nghttp3—Not affectedDocuments the limit as signaling-only

quic-go

quic-go is a QUIC/HTTP/3 library for Go. Its 1 MiB limit (DefaultMaxHeaderBytes) applied only to the compressed HEADERS frame.
The decoder returned every field at once without checking the size.
A 1,000,000-byte frame decoded into 160,075 fields, and about 15 MiB of heap (memory the program allocates at runtime) was still held after GC (garbage collection).

The GitHub advisory (GHSA-g754-hx8w-x2g6) says long names and values from the static table can expand a frame roughly 50x.
Both servers and clients are affected. The CVSS score is 5.3 (Moderate), and v0.56.0 and earlier are vulnerable.
Version 0.57.0 sends SETTINGS_MAX_FIELD_SECTION_SIZE and checks the size after each decoded entry.
When the limit is exceeded, a server returns 431 and a client resets the stream.

Cloudflare quiche

This is Cloudflare’s Rust implementation. Google’s QUIC implementation of the same name is a separate project (the Google quiche listed as not affected).
It counted size as name.len() + value.len() and left out the 32-byte overhead.
Under a 1 MiB limit, the spec allows about 32,000 one-byte headers, but quiche accepted about 1,000,000.

quiche also had a frame-handling bug unrelated to QPACK.
It preallocated memory based on the length declared in a frame header, before the payload arrived.
An attacker could send just the header of a 16 MiB SETTINGS or GOAWAY frame (the frame that announces a connection shutdown) and get 16 MiB allocated without sending the body.
The advisory (GHSA-4fgf-9xrr-88gf) rates it CVSS 7.5 (High).
Versions 0.1.0 and later are affected, and 0.29.3, published July 14, 2026, fixes it.

lsquic and OpenLiteSpeed

lsquic is LiteSpeed’s QUIC/HTTP/3 library in C, used by OpenLiteSpeed and LiteSpeed Web Server.
There was no cumulative limit on what each reference decoded to, and es_max_header_list_size defaulted to 0 (unlimited) and was never passed to the QPACK decoder anyway.
The 64 KiB cap also reset per HEADERS block instead of per stream.

The attack inserted one entry of about 4 KiB, then sent a chain of HEADERS frames with 15 one-byte references each.
Each block was small enough to stay under 64 KiB, and the counter went back to 0 at every new block.
In FoxIO’s PoC (proof-of-concept code), 50 connections and under 10 MiB of traffic pushed a container’s memory to its 5 GiB cap within a second.
The cgroup (the Linux mechanism that enforces per-process limits such as memory) memory.events file recorded the limit being hit more than 900 times.

lsquic 4.9.0, released July 15, 2026, added es_max_header_sets, which caps the number of buffered header sets and defaults to 1 on servers (CHANGELOG).
In the fingerprint list at the end of FoxIO’s post, which identifies server type and version, OpenLiteSpeed 1.9.1 was running lsquic 4.8.2.
According to the OpenLiteSpeed release notes, 1.9.2 (August 6) moved to lsquic 4.9.3 and 1.9.3 (September 30) to lsquic 4.10.0, so 1.9.2 and later include the 4.9.0 fix.

Bun

The Bun JavaScript runtime uses lsquic for HTTP/3, but it replaced lsquic’s 64 KiB cumulative check with a per-header check.
It advertises es_max_header_list_size=16384 to the peer but does not enforce it.
One connection with 100 streams and 60,000 references per stream (5.8 MiB on the wire in total) was enough to get a 2 GiB container OOM-killed.

A different bug takes even less traffic.
us_quic_hsi_prepare (a header-handling function on the Bun side) tracks the header buffer capacity in an unsigned int and doubles it when it runs out.
Past 2 GiB the value overflows to 0, doubling 0 gives 0, and the loop never ends.
A single stream of about 516 KiB (528,500 indexed references) left the server stuck at 100% CPU.

Bun received the report on May 24, 2026, but version 1.4.3 was still unpatched on October 20.
FoxIO’s workaround is to not expose Bun HTTP/3 servers, and the post suggests a fix that checks the total before appending to the buffer (if (h->len + space > 16 * 1024) return NULL;).

aioquic / mitmproxy

aioquic, a Python implementation, parsed and stored MAX_FIELD_SECTION_SIZE but never compared anything against it on receive.
Its flow control window (how much the peer is allowed to send) also keeps doubling with no ceiling.
A minimal aioquic server that throws headers away survives, but mitmproxy, the debugging proxy built on aioquic, keeps an http.Request per stream, so its memory use keeps growing.
FoxIO’s workaround for mitmproxy is to disable HTTP/3 with --set http3=false.

hyperium/h3

The h3 crate (a Rust library package) puts decoded fields into an http::HeaderMap.
HeaderMap::append panics (aborts the thread) with size overflows MAX_SIZE once it reaches 32,768 entries, and the default configuration set no header size limit at all.
The maintainer closed the report on December 5, 2025, saying the library is still alpha and the default is planned to change.
FoxIO’s workaround is to set a reasonable max_field_section_size.

Wireshark

The Wireshark packet analyzer crashed when it was fed the attack traffic built for quic-go.
It had no limit on the number of decoded headers, and its buffer concatenation (tvb_composite) was slow, so the HTTP/3 dissector hung.
The advisory (wnpa-sec-2025-07) says it happens when decrypting with a keylog file (a file of exported TLS secrets) or opening a capture that contains decryption secrets.
Versions 4.6.0 to 4.6.1 and 4.4.0 to 4.4.11 are affected, and 4.6.2 and 4.4.12, released December 3, 2025, fix it.

Implementations that were not affected

Google quiche enforced a 60K cap during decoding.
IIS (http.sys) also checked incrementally while decoding and kept its QPACK dynamic table at 4K.

FoxIO rated ngtcp2 / nghttp3 as not affected, but nghttp3 treats max_field_section_size as something it only advertises to the peer, and leaves enforcement on receive to the application.
After the report, it documented this on December 17, 2025 (nghttp3 PR #445).
nghttp3 does not count decoded header size, so a server built on it has to do that in its own code.

From first report to publication

DateEvent
November 9, 2025Reported to quic-go
November 14, 2025Reported to Wireshark
November 15, 2025Reported to Cloudflare quiche
November 21, 2025quic-go 0.57.0 released
December 3, 2025Wireshark 4.6.2 released
December 4, 2025Reported to hyperium/h3
December 5, 2025h3 maintainer closes the report
December 17, 2025nghttp3 documents the limit as signaling-only
May 24, 2026Reported to Bun
July 13, 2026Reported to aioquic and lsquic
July 14, 2026Cloudflare quiche 0.29.3 released
July 15, 2026lsquic 4.9.0 released
October 20, 2026FoxIO post published

The PoC is in headeraccordions under FoxIO-LLC/blog_poc.
The post also lists fingerprints for FoxIO’s JA4Scan-QUIC (a tool that identifies server software from its QUIC responses) that tell the affected implementations and versions apart.