From 822bf22224adbd662e8d0b865eeacb2b294d16cd Mon Sep 17 00:00:00 2001
From: Vladimir Lettiev <thecrux@gmail.com>
Date: Sun, 7 Jun 2026 12:09:19 +0300
Subject: [PATCH] Security fix for CVE-2026-10725 (http2 bomb)

On June 2, 2026, an attack method was disclosed that can cause denial of
service against popular HTTP/2 implementations by exhausting the web server
process's RAM for storing headers
(https://blog.calif.io/p/codex-discovered-a-hidden-http2-bomb). Sending
compressed headers from the attacker results in significant memory consumption
of the web server, the amplification factor can reach 5700, allowing the
attacker to disable the web server process within a few seconds, causing it to
crash or significant slowdown.

Robert Rothenberg (CPAN Security Group) discovered and reported that the
implementation of Protocol::HTTP2 also vulnerable to the http2 bomb attack.
CVE-2026-10725 was assigned to this issue.

RFC 7540 specifies that the SETTINGS_MAX_HEADER_LIST_SIZE can be used to
specify a recommended maximum value for the size of uncompressed headers. The
standard defines it as an infinite value by default, but leaves it to the
implementations of the http2 protocol to choose any value, as well as the
reaction to exceeding the value (error or ignoring). The Protocol::HTTP2
originally had a default limit of 65536 bytes, but this limit was not checked
in any way in the code.

To fix the vulnerability, the code now checks for exceeding the uncompressed
header size limit and terminates the connection with error if it is exceeded.
One can override the SETTINGS_MAX_HEADER_LIST_SIZE when establishing a new
connection:

    use Protocol::HTTP2::Constants qw(:settings)

    $server = Protocol::HTTP2::Server->new(
        settings => { &SETTINGS_MAX_HEADER_LIST_SIZE => 8192 },
    ...
---
 lib/Protocol/HTTP2/Frame/Continuation.pm | 14 ++++-
 lib/Protocol/HTTP2/HeaderCompression.pm  | 15 +++++
 t/15_headersize.t                        | 74 ++++++++++++++++++++++++
 3 files changed, 101 insertions(+), 2 deletions(-)
 create mode 100644 t/15_headersize.t

diff --git a/lib/Protocol/HTTP2/Frame/Continuation.pm b/lib/Protocol/HTTP2/Frame/Continuation.pm
index 7d40662..9c26aeb 100644
--- a/lib/Protocol/HTTP2/Frame/Continuation.pm
+++ b/lib/Protocol/HTTP2/Frame/Continuation.pm
@@ -1,7 +1,7 @@
 package Protocol::HTTP2::Frame::Continuation;
 use strict;
 use warnings;
-use Protocol::HTTP2::Constants qw(:flags :errors);
+use Protocol::HTTP2::Constants qw(:flags :errors :settings);
 use Protocol::HTTP2::Trace qw(tracer);
 
 sub decode {
@@ -17,9 +17,19 @@ sub decode {
         $con->error(PROTOCOL_ERROR);
         return undef;
     }
+    if (
+        # Headers compressed size already exceeded decompressed limit
+        length( $con->stream_header_block( $frame_ref->{stream} ) ) + $length >
+        $con->dec_setting(SETTINGS_MAX_HEADER_LIST_SIZE)
+      )
+    {
+        $con->error(ENHANCE_YOUR_CALM);
+        return undef;
+    }
 
     $con->stream_header_block_add( $frame_ref->{stream},
-        substr( $$buf_ref, $buf_offset, $length ) );
+        substr( $$buf_ref, $buf_offset, $length ) )
+      or return undef;
 
     # Stream header block complete
     $con->stream_headers_done( $frame_ref->{stream} )
diff --git a/lib/Protocol/HTTP2/HeaderCompression.pm b/lib/Protocol/HTTP2/HeaderCompression.pm
index c139f8b..865c946 100644
--- a/lib/Protocol/HTTP2/HeaderCompression.pm
+++ b/lib/Protocol/HTTP2/HeaderCompression.pm
@@ -139,6 +139,7 @@ sub headers_decode {
     my $eh = $context->{emitted_headers};
 
     my $offset = 0;
+    my $hsize  = 0;
 
     while ( $offset < $length ) {
 
@@ -163,6 +164,7 @@ sub headers_decode {
             # Static table or Header Table entry
             if ( $index <= @stable ) {
                 my ( $key, $value ) = @{ $stable[ $index - 1 ] };
+                $hsize += length($key) + length($value) + 32;
                 push @$eh, $key, $value;
                 tracer->debug("$key = $value\n");
             }
@@ -177,6 +179,7 @@ sub headers_decode {
             else {
                 my $kv_ref = $ht->[ $index - @stable - 1 ];
 
+                $hsize += length( $kv_ref->[0] ) + length( $kv_ref->[1] ) + 32;
                 push @$eh, @$kv_ref;
                 tracer->debug("$kv_ref->[0] = $kv_ref->[1]\n");
             }
@@ -209,6 +212,7 @@ sub headers_decode {
             last unless $value_size;
 
             # Emitting header
+            $hsize += length($key) + length($value) + 32;
             push @$eh, $key, $value;
 
             # Add to index
@@ -252,6 +256,7 @@ sub headers_decode {
             }
 
             # Emitting header
+            $hsize += length($key) + length($value) + 32;
             push @$eh, $key, $value;
 
             # Add to index
@@ -300,6 +305,16 @@ sub headers_decode {
             $con->error(COMPRESSION_ERROR);
             return undef;
         }
+
+        # Check header limit
+        if ( $hsize > $context->{settings}->{&SETTINGS_MAX_HEADER_LIST_SIZE} ) {
+            tracer->error( "Headers size has exceeded the allowed limit: "
+                  . $hsize
+                  . "\n" );
+            $con->error(ENHANCE_YOUR_CALM);
+            return undef;
+        }
+
     }
 
     if ( $offset != $length ) {
diff --git a/t/15_headersize.t b/t/15_headersize.t
new file mode 100644
index 0000000..0359603
--- /dev/null
+++ b/t/15_headersize.t
@@ -0,0 +1,74 @@
+use strict;
+use warnings;
+use Test::More;
+use Protocol::HTTP2::Client;
+use Protocol::HTTP2::Server;
+use Protocol::HTTP2::Constants qw(:errors :settings :limits);
+use lib 't/lib';
+use PH2Test qw(fake_connect random_string);
+
+subtest 'hpack bomb' => sub {
+
+    plan tests => 1;
+    my $hc = 2000;
+
+    my $server;
+    $server = Protocol::HTTP2::Server->new(
+        on_error => sub {
+            my $error = shift;
+            is $error, &ENHANCE_YOUR_CALM, "ENHANCE_YOUR_CALM error";
+        },
+        on_request => sub {
+            ok 0, "request should not have been received"
+        }
+    );
+
+    my $client = Protocol::HTTP2::Client->new;
+    $client->request(
+        ':scheme'    => 'http',
+        ':authority' => 'localhost:8000',
+        ':path'      => '/',
+        ':method' => 'GET',
+        headers   => [ ('a' => '')x$hc ],
+    );
+
+    fake_connect( $server, $client );
+};
+
+subtest 'change settings' => sub {
+
+    plan tests => 3;
+    my $hc = 2000;
+
+    my $server;
+    $server = Protocol::HTTP2::Server->new(
+        settings => {
+            &SETTINGS_MAX_HEADER_LIST_SIZE => $hc*33 + 200
+        },
+        on_error => sub {
+            my $error = shift;
+            ok 0, "should be no error";
+        },
+        on_request => sub {
+            my ( $stream_id, $headers, $data ) = @_;
+            my %h = (@$headers);
+            is $#$headers, 2*($hc+4)-1, "2*($hc + 4) headers";
+            is keys %h, 5, "merged in 1 + 4 headers";
+            ok exists $h{b}, "b header";
+        }
+    );
+
+    my $client = Protocol::HTTP2::Client->new;
+    $client->request(
+        ':scheme'    => 'http',
+        ':authority' => 'localhost:8000',
+        ':path'      => '/',
+        ':method' => 'GET',
+        headers   => [ ('b' => '')x$hc ],
+    );
+
+    fake_connect( $server, $client );
+};
+
+
+done_testing;
