From 822bf22224adbd662e8d0b865eeacb2b294d16cd Mon Sep 17 00:00:00 2001 From: Vladimir Lettiev 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;