Summary
hpax, the Elixir HPACK (HTTP/2 header compression) decoder used by the Mint/Bandit ecosystem, decodes HPACK variable-length integers with no cap on the decoded value or the number of continuation octets. An unauthenticated remote attacker who can send an HTTP/2 header block to any server using this decoder can supply a single integer built from a long run of continuation octets, forcing the decoder into superlinear (≈O(N²)) bignum arithmetic. Each small, cheaply-crafted request costs the server a large, attacker-controlled amount of CPU (a denial-of-service amplification).
Details
HPACK encodes an integer that does not fit in its prefix by continuing it across following octets: each octet with the high bit set contributes 7 more value bits and signals "more to come", and the first octet with the high bit clear terminates the integer.
The bug is in decode_remaining_integer/3 in lib/hpax/types.ex, reached from the integer entry point decode_integer/2 and ultimately from the public HPAX.decode/2 in lib/hpax.ex. The routine accumulates the running value as int + (value <<< m), where the shift amount m grows by 7 for every continuation octet consumed. The only stopping conditions are the terminating octet and a fallback clause that returns :error on truncated/empty input. Nothing halts decoding because the integer grew too large or because too many octets were consumed: there is no value cap and no octet-count cap.
Because BEAM integers are arbitrary precision, nothing overflows or wraps. Instead, for a run of N continuation octets the decoder builds an integer of O(N) bits, and because it re-adds into an ever-larger bignum on each of the N steps, total arithmetic cost is ≈O(N²). The oversized integer is harmless downstream (an implausibly large length simply fails the binary-size(length) match in decode_binary/1 and decode returns :error); the damage is the CPU and transient memory spent building the bignum before that point.
PoC
- Craft a raw HPACK header block fragment: one prefix octet
0xFF (indexed header field, 7-bit prefix all ones, so the integer continues), followed by N continuation octets 0xFF, terminated by a single 0x00.
- Pass the fragment to the public API
HPAX.decode(payload, HPAX.new(4096)).
- Time the decode across increasing N (e.g. 50k, 100k, 200k, 400k octets). Doubling N roughly quadruples the decode time (
time / N² stays constant), confirming the superlinear cost; a ~400 KB fragment takes seconds of CPU.
Impact
Any server or application that uses hpax to decode HPACK header blocks from untrusted peers (i.e. any HTTP/2 endpoint reachable by remote clients) is affected. An unauthenticated remote attacker can send small, cheaply-generated header blocks that each consume a large, superlinear amount of server CPU (and transiently memory), degrading or exhausting the service for other users. No privileges or special configuration are required.
References
Summary
hpax, the Elixir HPACK (HTTP/2 header compression) decoder used by the Mint/Bandit ecosystem, decodes HPACK variable-length integers with no cap on the decoded value or the number of continuation octets. An unauthenticated remote attacker who can send an HTTP/2 header block to any server using this decoder can supply a single integer built from a long run of continuation octets, forcing the decoder into superlinear (≈O(N²)) bignum arithmetic. Each small, cheaply-crafted request costs the server a large, attacker-controlled amount of CPU (a denial-of-service amplification).Details
HPACK encodes an integer that does not fit in its prefix by continuing it across following octets: each octet with the high bit set contributes 7 more value bits and signals "more to come", and the first octet with the high bit clear terminates the integer.
The bug is in
decode_remaining_integer/3inlib/hpax/types.ex, reached from the integer entry pointdecode_integer/2and ultimately from the publicHPAX.decode/2inlib/hpax.ex. The routine accumulates the running value asint + (value <<< m), where the shift amountmgrows by 7 for every continuation octet consumed. The only stopping conditions are the terminating octet and a fallback clause that returns:erroron truncated/empty input. Nothing halts decoding because the integer grew too large or because too many octets were consumed: there is no value cap and no octet-count cap.Because BEAM integers are arbitrary precision, nothing overflows or wraps. Instead, for a run of N continuation octets the decoder builds an integer of O(N) bits, and because it re-adds into an ever-larger bignum on each of the N steps, total arithmetic cost is ≈O(N²). The oversized integer is harmless downstream (an implausibly large
lengthsimply fails thebinary-size(length)match indecode_binary/1and decode returns:error); the damage is the CPU and transient memory spent building the bignum before that point.PoC
0xFF(indexed header field, 7-bit prefix all ones, so the integer continues), followed by N continuation octets0xFF, terminated by a single0x00.HPAX.decode(payload, HPAX.new(4096)).time / N²stays constant), confirming the superlinear cost; a ~400 KB fragment takes seconds of CPU.Impact
Any server or application that uses
hpaxto decode HPACK header blocks from untrusted peers (i.e. any HTTP/2 endpoint reachable by remote clients) is affected. An unauthenticated remote attacker can send small, cheaply-generated header blocks that each consume a large, superlinear amount of server CPU (and transiently memory), degrading or exhausting the service for other users. No privileges or special configuration are required.References