From: CPANSec Security Scanner Bot <cpan-security@security.metacpan.org>
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 <noreply@anthropic.com>

---
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;
