Skip to content

Commit e429405

Browse files
oaldersclaude
andcommitted
Enforce max_body_size on the deflate path (CVE-2026-65060)
decoded_content() did not apply max_body_size (or the global $HTTP::Message::MAXIMUM_BODY_SIZE) to "Content-Encoding: deflate", nor to its raw-deflate fallback, while gzip, x-gzip and bzip2 all honoured it. A hostile server could therefore bypass the limit entirely just by labelling a decompression bomb "deflate". Verified pre-patch: a 4GB bomb exhausts memory outright; post-patch it dies in 0.14s at 20.9MB RSS. Rather than reimplement inflate on Compress::Raw::Zlib, bound the existing IO::Uncompress decoder with a new _uncompress_bounded() helper. That keeps one decode path, so multi-member streams, trailing garbage and transparent pass-through all behave exactly as before and the cap is purely an added constraint. Confirmed byte-identical output against the unpatched code on eight unlimited-path edge cases. Reads are chunked so a bomb is caught early. Output can overshoot the limit by at most one chunk (32K, and never more than the limit itself) before it is noticed, which matches how the gzip and bzip2 branches already behave. Also reject a negative max_body_size. Each decoder reacted to one differently and some quietly returned no content at all, which is indistinguishable from a successful decode. max_body_size() and $HTTP::Message::MAXIMUM_BODY_SIZE were undocumented; they now have Pod covering the bomb rationale, the one-chunk overshoot, which encodings enforce the limit, why base64 and quoted-printable are exempt, and that br cannot distinguish an over-limit body from a corrupt one. GH#206, reported by grr. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c200e42 commit e429405

3 files changed

Lines changed: 248 additions & 8 deletions

File tree

‎Changes‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Revision history for HTTP-Message
22

33
{{$NEXT}}
4+
- SECURITY: CVE-2026-65060. decoded_content() did not enforce
5+
max_body_size (or $HTTP::Message::MAXIMUM_BODY_SIZE) on the "deflate"
6+
Content-Encoding, so a decompression bomb labelled "deflate" bypassed
7+
the limit that gzip and bzip2 already honoured. The deflate and
8+
raw-deflate paths are now bounded by the same limit. (GH#206)
9+
(reported by grr)
10+
- Reject a negative max_body_size instead of quietly decoding to nothing
11+
- Document max_body_size() and $HTTP::Message::MAXIMUM_BODY_SIZE
412

513
7.03 2026-07-21 20:45:16Z
614
- Fix max_body_size for Content-Encoding: br, which made every brotli

‎lib/HTTP/Message.pm‎

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,37 @@ sub _set_max_body_size {
291291
$self->{_max_body_size} = $_[1];
292292
}
293293

294+
# Uncompress $$input_ref with one of the IO::Uncompress::* classes, refusing
295+
# to produce more than $limit octets of output. Returns a reference to the
296+
# decoded content, or undef if $class could not handle the input. Croaks if
297+
# the content would decode to more than $limit octets; that is what makes
298+
# max_body_size a defence against decompression bombs on these encodings.
299+
sub _uncompress_bounded {
300+
my($class, $input_ref, $limit, %options) = @_;
301+
302+
# Unlike the Compress::Raw::* adapters used for gzip and bzip2 above,
303+
# these classes read their input without consuming it, so $input_ref can
304+
# be handed over as is. That matters: the deflate branch below calls us a
305+
# second time, with the same input, to retry as raw deflate.
306+
my $z = $class->new($input_ref, %options) or return undef;
307+
308+
# Chunk the reads so a bomb is caught early. The output can overshoot
309+
# $limit by up to one chunk before we notice, hence the cap on $bufsize.
310+
my $bufsize = 32 * 1024;
311+
$bufsize = $limit + 1 if defined $limit && $limit < $bufsize;
312+
313+
my $output = q{};
314+
while (1) {
315+
my $read = $z->read(my $chunk, $bufsize);
316+
return undef if !defined $read || $read < 0;
317+
last if $read == 0;
318+
$output .= $chunk;
319+
Carp::croak("Decoded content would be larger than $limit octets")
320+
if defined $limit && length($output) > $limit;
321+
}
322+
return \$output;
323+
}
324+
294325
sub decoded_content
295326
{
296327
my($self, %opt) = @_;
@@ -305,6 +336,12 @@ sub decoded_content
305336
: defined $self->max_body_size ? $self->max_body_size
306337
: undef
307338
;
339+
# A negative limit is a caller error, and every decoder reacts to one
340+
# differently -- some quietly hand back no content at all, which looks
341+
# just like a successful decode. Reject it up front instead.
342+
die 'max_body_size must not be negative'
343+
if defined $content_limit && $content_limit < 0;
344+
308345
my %limiter_options;
309346
if( defined $content_limit ) {
310347
%limiter_options = (LimitOutput => 1, Bufsize => $content_limit);
@@ -395,24 +432,27 @@ sub decoded_content
395432
}
396433
elsif ($ce eq "deflate") {
397434
require IO::Uncompress::Inflate;
398-
my $output;
399-
my $status = IO::Uncompress::Inflate::inflate($content_ref, \$output, Transparent => 0);
435+
my $output_ref = _uncompress_bounded(
436+
'IO::Uncompress::Inflate', $content_ref, $content_limit,
437+
Transparent => 0,
438+
);
400439
my $error = $IO::Uncompress::Inflate::InflateError;
401-
unless ($status) {
440+
unless ($output_ref) {
402441
# "Content-Encoding: deflate" is supposed to mean the
403442
# "zlib" format of RFC 1950, but Microsoft got that
404443
# wrong, so some servers sends the raw compressed
405444
# "deflate" data. This tries to inflate this format.
406-
$output = undef;
407445
require IO::Uncompress::RawInflate;
408-
unless (IO::Uncompress::RawInflate::rawinflate($content_ref, \$output)) {
446+
$output_ref = _uncompress_bounded(
447+
'IO::Uncompress::RawInflate', $content_ref, $content_limit,
448+
);
449+
unless ($output_ref) {
409450
$self->push_header("Client-Warning" =>
410451
"Could not raw inflate content: $IO::Uncompress::RawInflate::RawInflateError");
411-
$output = undef;
412452
}
413453
}
414-
die "Can't inflate content: $error" unless defined $output;
415-
$content_ref = \$output;
454+
die "Can't inflate content: $error" unless $output_ref;
455+
$content_ref = $output_ref;
416456
$content_ref_iscopy++;
417457
}
418458
elsif ($ce eq "compress" || $ce eq "x-compress") {
@@ -1042,8 +1082,49 @@ If TRUE then a reference to decoded content is returned. This might
10421082
be more efficient in cases where the decoded content is identical to
10431083
the raw content as no data copying is required in this case.
10441084
1085+
=item C<max_body_size>
1086+
1087+
The maximum size, in octets, that the content is allowed to decode to.
1088+
This overrides the value set through max_body_size() for this call only.
1089+
See max_body_size() for details.
1090+
10451091
=back
10461092
1093+
=item $mess->max_body_size
1094+
1095+
=item $mess->max_body_size( $size )
1096+
1097+
Get/set the maximum size, in octets, that decoded_content() will allow the
1098+
content to decode to. A C<Content-Encoding> can compress hugely -- a few
1099+
kilobytes on the wire can inflate to gigabytes in memory -- so a hostile or
1100+
compromised server can exhaust your process's memory with a single response.
1101+
This limit is the defence against such a "decompression bomb": if the content
1102+
would decode to more than $size octets, decoded_content() aborts instead of
1103+
allocating it. Decoding works in blocks, so it can overshoot the limit by up
1104+
to one block -- at most 32 kilobytes, and never more than $size itself --
1105+
before it notices and aborts. Leave some headroom. A negative $size is an
1106+
error.
1107+
1108+
The limit is enforced for the C<gzip>, C<x-gzip>, C<deflate>, C<br>, C<bzip2>
1109+
and C<x-bzip2> encodings. It is not applied to content that needs no decoding,
1110+
nor to the charset decoding step. The C<base64> and C<quoted-printable>
1111+
encodings are also exempt: neither can expand its input, so neither can be
1112+
used to mount this kind of attack.
1113+
1114+
Note that C<br> cannot distinguish content that exceeds the limit from content
1115+
that is simply corrupt, and reports both the same way.
1116+
1117+
The default is taken from C<$HTTP::Message::MAXIMUM_BODY_SIZE> when the
1118+
message is constructed. That variable is unset by default, meaning content is
1119+
decoded without any limit; set it if you want every message to be capped. Set
1120+
either to C<undef> to remove the limit again.
1121+
1122+
$HTTP::Message::MAXIMUM_BODY_SIZE = 100 * 1024 * 1024;
1123+
1124+
Since a response whose content exceeds the limit yields no content at all, it
1125+
is usually worth passing C<raise_error> to decoded_content() so you can tell
1126+
the two cases apart.
1127+
10471128
=item $mess->decodable
10481129
10491130
=item HTTP::Message::decodable()

‎t/message-decode-deflatebomb.t‎

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# CVE-2026-65060: max_body_size was not enforced on the "deflate" path,
2+
# so a decompression bomb labelled Content-Encoding: deflate bypassed the
3+
# cap that gzip/bzip2/brotli honour.
4+
5+
use strict;
6+
use warnings;
7+
8+
use Test::More;
9+
10+
use HTTP::Headers qw( );
11+
use HTTP::Response qw( );
12+
13+
use Test::Needs {
14+
'IO::Compress::Deflate' => 0,
15+
'IO::Compress::RawDeflate' => 0,
16+
};
17+
18+
require IO::Compress::Deflate;
19+
require IO::Compress::RawDeflate;
20+
21+
my $size = 16 * 1024 * 1024;
22+
my $stream = "\0" x $size;
23+
24+
IO::Compress::Deflate::deflate( \$stream, \my $bomb, Level => 9 )
25+
or die "Can't deflate content: $IO::Compress::Deflate::DeflateError";
26+
27+
IO::Compress::RawDeflate::rawdeflate( \$stream, \my $raw_bomb, Level => 9 )
28+
or die
29+
"Can't rawdeflate content: $IO::Compress::RawDeflate::RawDeflateError";
30+
31+
sub response_for {
32+
my ( $body, $max_body_size ) = @_;
33+
my $headers = HTTP::Headers->new(
34+
Content_Type => 'application/octet-stream',
35+
Content_Encoding => 'deflate',
36+
);
37+
my $response = HTTP::Response->new( 200, 'OK', $headers, $body );
38+
$response->max_body_size($max_body_size);
39+
return $response;
40+
}
41+
42+
# Self-test: without a limit the bomb decodes in full.
43+
{
44+
my $content = response_for( $bomb, undef )->decoded_content;
45+
is( length $content, $size,
46+
'Self-test: zlib-deflate bomb decodes in full' );
47+
48+
$content = response_for( $raw_bomb, undef )->decoded_content;
49+
is( length $content, $size,
50+
'Self-test: raw-deflate bomb decodes in full' );
51+
}
52+
53+
# The cap must be enforced, exactly as it is for gzip.
54+
{
55+
my $response = response_for( $bomb, 512 * 1024 );
56+
my $content;
57+
my $lives = eval {
58+
$content = $response->decoded_content( raise_error => 1 );
59+
1;
60+
};
61+
is( $lives, undef,
62+
'We die decoding a zlib-deflate body larger than max_body_size' );
63+
like( $@, qr/larger than 524288 octets/,
64+
'... and the error names the limit' );
65+
}
66+
67+
{
68+
my $response = response_for( $raw_bomb, 512 * 1024 );
69+
my $content;
70+
my $lives = eval {
71+
$content = $response->decoded_content( raise_error => 1 );
72+
1;
73+
};
74+
is( $lives, undef,
75+
'We die decoding a raw-deflate body larger than max_body_size' );
76+
like( $@, qr/larger than 524288 octets/,
77+
'... and the error names the limit' );
78+
}
79+
80+
# The per-call parameter works too.
81+
{
82+
my $response = response_for( $bomb, undef );
83+
my $lives = eval {
84+
$response->decoded_content(
85+
raise_error => 1,
86+
max_body_size => 512 * 1024,
87+
);
88+
1;
89+
};
90+
is( $lives, undef, 'The max_body_size parameter caps a deflate body' );
91+
}
92+
93+
# Without raise_error we get undef rather than a bomb.
94+
{
95+
my $response = response_for( $bomb, 512 * 1024 );
96+
ok( !defined $response->decoded_content( raise_error => 0 ),
97+
'Without raise_error an over-large deflate body decodes to undef' );
98+
}
99+
100+
# Content that fits under the cap still decodes correctly.
101+
{
102+
my $body = 'x' x 1000;
103+
104+
IO::Compress::Deflate::deflate( \$body, \my $small )
105+
or die "Can't deflate content: $IO::Compress::Deflate::DeflateError";
106+
my $response = response_for( $small, 512 * 1024 );
107+
is( $response->decoded_content( raise_error => 1 ), $body,
108+
'A zlib-deflate body under the cap decodes correctly' );
109+
110+
IO::Compress::RawDeflate::rawdeflate( \$body, \my $small_raw )
111+
or die
112+
"Can't rawdeflate content: $IO::Compress::RawDeflate::RawDeflateError";
113+
$response = response_for( $small_raw, 512 * 1024 );
114+
is( $response->decoded_content( raise_error => 1 ), $body,
115+
'A raw-deflate body under the cap decodes correctly' );
116+
}
117+
118+
# A negative limit is a caller error. It has to fail loudly: silently
119+
# handing back empty content would look like a successful decode.
120+
{
121+
for my $limit ( -1, -1000 ) {
122+
my $response = response_for( $bomb, $limit );
123+
my $lives = eval {
124+
$response->decoded_content( raise_error => 1 );
125+
1;
126+
};
127+
is( $lives, undef, "A max_body_size of $limit is rejected" );
128+
}
129+
}
130+
131+
# Content exactly at the cap is not rejected.
132+
{
133+
my $body = 'x' x 1024;
134+
IO::Compress::Deflate::deflate( \$body, \my $small )
135+
or die "Can't deflate content: $IO::Compress::Deflate::DeflateError";
136+
my $response = response_for( $small, 1024 );
137+
is( $response->decoded_content( raise_error => 1 ), $body,
138+
'A deflate body exactly at the cap decodes correctly' );
139+
}
140+
141+
# The raw-deflate fallback passes undecodable content through untouched
142+
# (IO::Uncompress::RawInflate's Transparent mode). Keep that behaviour: such
143+
# content is not amplified, so the cap has nothing to protect against.
144+
{
145+
my $body = 'this is not deflated at all';
146+
my $response = response_for( $body, 512 * 1024 );
147+
is( $response->decoded_content( raise_error => 1 ), $body,
148+
'Undecodable content is passed through unchanged' );
149+
}
150+
151+
done_testing();

0 commit comments

Comments
 (0)