Vulnerability EEF-CVE-2026-113431

Unknown
UNKNOWN RISK
Vulnerabilities without an assigned CVSS score. Severity is not determinable from available data.
2 hours ago
November 06, 2026 at 10:28 AM UTC
Unbounded decoded HTTP/2 header-list size in cowlib cow_http2_machine enables HPACK amplification memory exhaustion
2.7.0 - 2.21.0
2.7.0 - 2.21.0

Summary

Unbounded decoded HTTP/2 header-list size in cowlib cow_http2_machine enables HPACK amplification memory exhaustion

Details

Summary

Allocation of Resources Without Limits or Throttling vulnerability in ninenines cowlib allows an unauthenticated remote peer to exhaust the memory of an HTTP/2 endpoint via HPACK-indexed header fields.

cow_http2_machine:headers_decode/4 in src/cow_http2_machine.erl decodes every HEADERS block with cow_hpack:decode/3. It bounds the compressed block size and the number of fields, but not the size of the decoded header list that RFC 9113 section 6.5.2 places under SETTINGS_MAX_HEADER_LIST_SIZE. A peer stores one header value of up to the dynamic table size (4096 bytes by default) and then references it with one-byte indexed fields, so about 100 bytes on the wire decode to a header list of about 390 KB. A consumer that copies the decoded values, as Cowboy does when cowboy_http2:headers_to_map/2 joins repeated cookie fields into one binary, allocates that amount for every stream. The total across streams and connections is not bounded.

This issue affects cowlib: from 2.7.0 onward.

Details

1. The decoder bounds everything but the decoded size. cow_http2_machine:headers_decode/4 passes each complete header block to cow_hpack:decode/3. Two limits apply: max_fragmented_header_block_size (32768 bytes by default) on the compressed block, and max_headers (100 by default, added in cowlib 2.17.0) on the number of fields. max_header_list_size appears in the module's option list with a default of infinity, next to a @todo comment, and is neither advertised in SETTINGS nor enforced on the decoded list, which is what RFC 9113 section 6.5.2 defines it for.

2. One-byte fields expand to kilobytes. The dynamic table holds entries of up to max_decode_table_size (4096 bytes by default). The peer sends one literal field with incremental indexing carrying a 4000-byte value, which becomes table entry 62. Every later field is the single byte 0xBE, an indexed representation of that entry. A 110-byte block that carries four pseudo-headers and 96 such references decodes to a 100-field list whose values total 384,000 bytes. Before 2.17.0 no field count applied, so a 32 KB block referenced the entry about 32,000 times.

3. The allocation is realised by the consumer. The list that cow_hpack returns is cheap, about 2 KB of heap, because every reference shares the one table binary. Cowboy's cowboy_http2:headers_to_map/2 joins repeated fields into a single new binary ("; " for cookie, ", " for other names), so each stream yields a 384,190-byte cookie value that lives in the request process for the life of the stream. Cowboy's HTTP/1.1 limits max_header_value_length and max_headers do not apply to HTTP/2. With the default max_concurrent_streams of 100 a connection holds about 38 MB, and only the Ranch connection limit bounds the total. Gun hands the decoded list to the owner process without copying the values, so the client side sees a small list per response and no amplification.

4. Measurement. On cowboy 2.20.0 with cowlib 2.21.0 on Erlang/OTP 29, one h2c connection on loopback sent 15,943 bytes of HEADERS for 100 streams. Binary memory grew by 38.4 MB and every handler received a 384,190-byte cookie header. A control with the same 100 streams and no indexed fields grew by 0.

Proof of concept

  1. Start a Cowboy clear listener with default protocol options and a handler that takes a few seconds to reply, so streams stay open.
  2. Open one TCP connection, send the HTTP/2 client preface and an empty SETTINGS frame.
  3. On stream 1 send a HEADERS frame with END_HEADERS and END_STREAM whose block contains :method, :scheme, :path and :authority, then one literal cookie field with incremental indexing carrying a 4000-byte value, then 95 indexed fields 0xBE referencing it.
  4. On streams 3 through 199 send the same frame with the four pseudo-headers followed by 96 indexed fields 0xBE. Each block is 110 bytes.
  5. Watch erlang:memory(binary) on the server. It grows by about 38 MB for about 16 KB sent, and cowboy_req:header(<<"cookie">>, Req) in each handler returns a 384,190-byte binary.

Impact

A remote unauthenticated client makes an HTTP/2 server built on cowlib, such as a Cowboy listener, allocate roughly 2,400 times the bytes it sends. One connection reaches about 38 MB with default settings, and an attacker holding many connections can drive the Erlang VM out of memory.

Workarounds

Cowboy servers that do not need HTTP/2 can set protocols => [http] in the protocol options. The listener then answers the HTTP/2 preface with 501 Not Implemented and the decoder is never reached.

Lowering max_headers in the HTTP/2 protocol options shrinks the per-stream allocation in proportion, to about max_headers times the 4096-byte table entry. This reduces the amplification but does not remove it.

Impacted packages

Timeline

Published
2 hours ago
November 06, 2026 at 10:28 AM UTC
Last Modified
2 hours ago
November 06, 2026 at 10:30 AM UTC