From: CPANSec Security Scanner Bot Subject: [PATCH] Protocol::HTTP2: enforce MAX_HEADER_LIST_SIZE + cap CONTINUATION accumulation (HTTP/2 HPACK bomb) The HPACK decoder materialised a full header copy per indexed reference with no limit, and the per-stream header block accumulated CONTINUATION frames unbounded — the HTTP/2 "bomb" (HPACK count amplification + CONTINUATION flood, remote memory-DoS). MAX_HEADER_LIST_SIZE was advertised in SETTINGS but never enforced on decode. Fix, two parts: - HeaderCompression.pm headers_decode: track the running emitted header-list size (RFC 7541 4.1: name_len + value_len + 32) and reject with ENHANCE_YOUR_CALM once it exceeds the connection's advertised SETTINGS_MAX_HEADER_LIST_SIZE, before materialising further headers. - Stream.pm stream_header_block_add: cap the accumulated HEADERS + CONTINUATION block at MAX_HEADER_LIST_SIZE (the compressed block cannot legitimately exceed the decoded-list limit), rejecting a CONTINUATION flood before it buffers unbounded memory. Co-Authored-By: Claude Opus 4.8 --- diff --git a/lib/Protocol/HTTP2/HeaderCompression.pm b/lib/Protocol/HTTP2/HeaderCompression.pm index c139f8b..5d420e2 100644 --- a/lib/Protocol/HTTP2/HeaderCompression.pm +++ b/lib/Protocol/HTTP2/HeaderCompression.pm @@ -137,6 +137,8 @@ sub headers_decode { my $ht = $context->{header_table}; my $eh = $context->{emitted_headers}; + my $max_list_size = $con->dec_setting(SETTINGS_MAX_HEADER_LIST_SIZE); + my $list_size = 0; my $offset = 0; @@ -164,6 +166,8 @@ sub headers_decode { if ( $index <= @stable ) { my ( $key, $value ) = @{ $stable[ $index - 1 ] }; push @$eh, $key, $value; + $list_size += 32 + length($eh->[-2]) + length($eh->[-1]); + if ($list_size > $max_list_size) { tracer->error("MAX_HEADER_LIST_SIZE exceeded\n"); $con->error(ENHANCE_YOUR_CALM); return undef; } tracer->debug("$key = $value\n"); } elsif ( $index > @stable + @$ht ) { @@ -178,6 +182,8 @@ sub headers_decode { my $kv_ref = $ht->[ $index - @stable - 1 ]; push @$eh, @$kv_ref; + $list_size += 32 + length($eh->[-2]) + length($eh->[-1]); + if ($list_size > $max_list_size) { tracer->error("MAX_HEADER_LIST_SIZE exceeded\n"); $con->error(ENHANCE_YOUR_CALM); return undef; } tracer->debug("$kv_ref->[0] = $kv_ref->[1]\n"); } @@ -210,6 +216,8 @@ sub headers_decode { # Emitting header push @$eh, $key, $value; + $list_size += 32 + length($eh->[-2]) + length($eh->[-1]); + if ($list_size > $max_list_size) { tracer->error("MAX_HEADER_LIST_SIZE exceeded\n"); $con->error(ENHANCE_YOUR_CALM); return undef; } # Add to index if ( $f == 0x40 ) { @@ -253,6 +261,8 @@ sub headers_decode { # Emitting header push @$eh, $key, $value; + $list_size += 32 + length($eh->[-2]) + length($eh->[-1]); + if ($list_size > $max_list_size) { tracer->error("MAX_HEADER_LIST_SIZE exceeded\n"); $con->error(ENHANCE_YOUR_CALM); return undef; } # Add to index if ( ( $f & 0xC0 ) == 0x40 ) { diff --git a/lib/Protocol/HTTP2/Stream.pm b/lib/Protocol/HTTP2/Stream.pm index 2599f06..713af57 100644 --- a/lib/Protocol/HTTP2/Stream.pm +++ b/lib/Protocol/HTTP2/Stream.pm @@ -415,7 +415,18 @@ sub stream_header_block_add { my ( $self, $stream_id, $header_chunk ) = @_; return undef if !exists $self->{streams}->{$stream_id} || !defined $header_chunk; - $self->{streams}->{$stream_id}->{header_block} .= $header_chunk; + my $block = \$self->{streams}->{$stream_id}->{header_block}; + $$block .= $header_chunk; + + # Cap the accumulated (HEADERS + CONTINUATION) header block to prevent a + # CONTINUATION flood from buffering unbounded memory before decode. The + # compressed block cannot legitimately exceed the decoded header-list + # limit, so MAX_HEADER_LIST_SIZE is a safe bound. + if ( length($$block) > $self->dec_setting(SETTINGS_MAX_HEADER_LIST_SIZE) ) { + tracer->error("Accumulated header block exceeds MAX_HEADER_LIST_SIZE\n"); + $self->error(ENHANCE_YOUR_CALM); + return undef; + } } 1;