QPACK Header Accordions Exhaust Memory in 7 of 10 Tested HTTP/3 Implementations
Contents
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
- Update quic-go, quiche, lsquic/OpenLiteSpeed and Wireshark to the versions above
- Bun: do not expose HTTP/3 servers
- mitmproxy: disable HTTP/3 with
--set http3=false - 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
| Mistake | What happens |
|---|---|
| Checking only the compressed frame size | A small frame can expand into a huge header section without being stopped |
| Decoding everything before checking | The memory is already allocated by the time the check runs |
| Leaving out the 32-byte overhead | Lots of 1-byte headers fit dozens of times over the limit |
| Defaulting to no limit when unconfigured | Nothing is enforced with the default settings |
| Never enforcing a configured limit | The 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
| Implementation | CVE | Impact reported by FoxIO | Cause |
|---|---|---|---|
| quic-go | CVE-2025-64702 | ~15x retained heap | Only checks the compressed size |
| Cloudflare quiche | CVE-2026-12523 | ~20x | Missing 32-byte overhead, preallocation from declared frame length |
| lsquic | None (fixed) | ~600-1800x | No cumulative decoded limit, unlimited HEADERS blocks |
| Bun | None | ~4000x | Built on lsquic but removes lsquic’s cumulative cap |
| aioquic / mitmproxy | None | ~20-30x | Limit is parsed but never enforced |
| hyperium/h3 | None (reported, not fixed) | Thread panic | HeaderMap::append panics at 32,768 entries |
| Wireshark | CVE-2025-13945 | Crash | HTTP/3 dissector does not bound decoded headers |
| Google quiche | — | Not affected | Enforces its cap during decoding |
| IIS (http.sys) | — | Not affected | Checks incrementally while decoding, 4K QPACK table |
| ngtcp2 / nghttp3 | — | Not affected | Documents 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
| Date | Event |
|---|---|
| November 9, 2025 | Reported to quic-go |
| November 14, 2025 | Reported to Wireshark |
| November 15, 2025 | Reported to Cloudflare quiche |
| November 21, 2025 | quic-go 0.57.0 released |
| December 3, 2025 | Wireshark 4.6.2 released |
| December 4, 2025 | Reported to hyperium/h3 |
| December 5, 2025 | h3 maintainer closes the report |
| December 17, 2025 | nghttp3 documents the limit as signaling-only |
| May 24, 2026 | Reported to Bun |
| July 13, 2026 | Reported to aioquic and lsquic |
| July 14, 2026 | Cloudflare quiche 0.29.3 released |
| July 15, 2026 | lsquic 4.9.0 released |
| October 20, 2026 | FoxIO 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.