EEF-CVE-2026-113431: Unbounded decoded HTTP/2 header-list size in cowlib cow_http2_machine enables HPACK amplification memory exhaustion

Published Nov 6, 2026
·
Updated

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.

cowhttp2machine:headersdecode/4 in src/cowhttp2machine.erl decodes every HEADERS block with cowhpack: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 SETTINGSMAXHEADERLISTSIZE. 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 cowboyhttp2:headerstomap/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. cowhttp2machine:headersdecode/4 passes each complete header block to cowhpack:decode/3. Two limits apply: maxfragmentedheaderblocksize (32768 bytes by default) on the compressed block, and maxheaders (100 by default, added in cowlib 2.17.0) on the number of fields. maxheaderlistsize 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 maxdecodetablesize (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 cowhpack returns is cheap, about 2 KB of heap, because every reference shares the one table binary. Cowboy's cowboyhttp2:headerstomap/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 maxheadervaluelength and maxheaders do not apply to HTTP/2. With the default maxconcurrentstreams 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 ENDHEADERS and ENDSTREAM 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 cowboyreq: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 maxheaders in the HTTP/2 protocol options shrinks the per-stream allocation in proportion, to about maxheaders times the 4096-byte table entry. This reduces the amplification but does not remove it.

Affected Software

1 affected component
erlang/cowlib>=2.7.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Configuration

    Set the protocol options to protocols => [http] to disable HTTP/2 when it is not required.

    Cowboy listener protocols = [http]
  2. Configuration

    Lower max_headers from its default of 100 in the HTTP/2 protocol options to reduce the per-stream decoded header allocation.

    Cowboy HTTP/2 max_headers = A value lower than 100

Event History

Nov 6, 2026
CVE Published
via OSV·10:28 AM
Data Sourced
via OSV·10:28 AM
DescriptionSeverityWeaknessAffected Software

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203