{"affected":[{"package":{"ecosystem":"Hex","name":"cowlib","purl":"pkg:hex/cowlib"},"ranges":[{"events":[{"introduced":"2.7.0"}],"type":"SEMVER"}],"versions":["2.7.0","2.7.1","2.7.2","2.7.3","2.8.0","2.9.0","2.9.1","2.10.0","2.10.1","2.11.0","2.12.0","2.12.1","2.13.0","2.14.0","2.15.0","2.16.0","2.16.1","2.17.0","2.17.1","2.18.0","2.19.0","2.20.0","2.21.0"]},{"ranges":[{"events":[{"introduced":"b461b119e78e4e09bb28b186b09da7ed4a86a0dd"},{"fixed":"a3b15e29b3067644bfd580246c87f7af7a90b3b8"}],"repo":"https://github.com/ninenines/cowlib","type":"GIT"}]}],"aliases":["CVE-2026-113431"],"credits":[{"name":"Andrew Moore","type":"FINDER"},{"name":"Andrew Moore","type":"REPORTER"},{"name":"Loïc Hoguin","type":"REMEDIATION_DEVELOPER"},{"name":"Jonatan Männchen / EEF","type":"COORDINATOR"}],"database_specific":{"capec_ids":["CAPEC-491"],"cpe_ids":["cpe:2.3:a:ninenines:cowlib:*:*:*:*:*:*:*:*"],"cwe_ids":["CWE-770"]},"details":"## Summary\n\nAllocation 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.\n\n`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.\n\nThis issue affects cowlib: from 2.7.0 onward.\n\n## Details\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\n## Proof of concept\n\n1. Start a Cowboy clear listener with default protocol options and a handler that takes a few seconds to reply, so streams stay open.\n2. Open one TCP connection, send the HTTP/2 client preface and an empty SETTINGS frame.\n3. 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.\n4. 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.\n5. 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.\n\n## Impact\n\nA 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.\n\n## Workarounds\n\nCowboy 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.\n\nLowering `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.","id":"EEF-CVE-2026-113431","modified":"2026-11-06T10:28:17.110095Z","published":"2026-11-06T10:28:15.837Z","references":[{"type":"WEB","url":"https://cna.erlef.org/cves/CVE-2026-113431.html"},{"type":"WEB","url":"https://github.com/ninenines/cowlib/commit/b461b119e78e4e09bb28b186b09da7ed4a86a0dd"},{"type":"FIX","url":"https://github.com/ninenines/cowlib/commit/a3b15e29b3067644bfd580246c87f7af7a90b3b8"},{"type":"PACKAGE","url":"https://hex.pm/packages/cowlib"}],"related":[],"schema_version":"1.7.3","severity":[{"score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","type":"CVSS_V4"}],"summary":"Unbounded decoded HTTP/2 header-list size in cowlib cow_http2_machine enables HPACK amplification memory exhaustion","upstream":[]}