From 7719986de1c4ac1cc9dfe30159dc6cacd8fc8f00 Mon Sep 17 00:00:00 2001 From: Mat Trudel Date: Fri, 24 Jul 2026 21:39:23 -0400 Subject: [PATCH] Raise a catchable error on malformed Huffman-coded input HPAX.Huffman.decode/1 only handled two "end of input" cases: an exhausted binary, and 1-7 leftover bits that form valid EOS padding. Any input that leaves 8 or more trailing bits which don't correspond to a complete Huffman code - which can only happen with a malformed or malicious encoding, since a real encoder never produces such output - matched no clause at all, raising an unhandled FunctionClauseError instead of the {:hpax, _} throw that callers of HPAX.decode/2 already know how to handle gracefully. A peer can trigger this by sending a HEADERS frame with a deliberately invalid Huffman-coded string value, crashing the decoding process instead of the connection being cleanly failed per RFC 7541. --- lib/hpax/huffman.ex | 8 ++++++++ test/hpax/huffman_test.exs | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/lib/hpax/huffman.ex b/lib/hpax/huffman.ex index 574112f..2f9864f 100644 --- a/lib/hpax/huffman.ex +++ b/lib/hpax/huffman.ex @@ -85,6 +85,14 @@ defmodule HPAX.Huffman do end end + # Anything left over here is 8 or more bits that don't match any Huffman code (the clauses + # above only match a complete code, the empty binary, or up to 7 bits of valid EOS padding). + # This can only happen with a malformed/malicious encoding, since a real encoder never + # produces output with more than 7 trailing bits that aren't a complete code. + def decode(<<_rest::bitstring>>) do + throw({:hpax, {:protocol_error, :invalid_huffman_encoding}}) + end + ## Helpers @compile {:inline, take_significant_bits: 3} diff --git a/test/hpax/huffman_test.exs b/test/hpax/huffman_test.exs index 05cab8c..317c14e 100644 --- a/test/hpax/huffman_test.exs +++ b/test/hpax/huffman_test.exs @@ -19,4 +19,17 @@ defmodule HPAX.HuffmanTest do assert Huffman.decode(encoded) == :huffman.decode(encoded) end end + + describe "decode/1 with malformed input" do + test "raises a catchable :hpax error instead of a FunctionClauseError when the trailing bits don't correspond to any complete code" do + # 8+ bits of all 1s is a valid prefix of the 30-bit EOS code but is neither a complete + # code nor <= 7 bits of valid EOS padding, so it can't come from a real encoder + for n_bits <- [8, 9, 15, 16, 23, 29] do + invalid = for _ <- 1..n_bits, into: <<>>, do: <<1::1>> + + assert catch_throw(Huffman.decode(invalid)) == + {:hpax, {:protocol_error, :invalid_huffman_encoding}} + end + end + end end