Add ->max_body_size accessor
Limit decoded body size by manually decoding the compressed content
This creates one (more) copy of the content if we limit the output
because Zlib and Bzip2 want to remove the consumed input from the
input string.
Also, this moves away from IO::Uncompress::Gunzip and IO::Uncompress::Bzip2
in favour of Compress::Raw::Zlib and Compress::Raw::Bzip2 because I
found no way to convince IO::Uncompress::Gunzip::gunzip to pass through
the appropriate limiting options.
The API is extended (but not yet documented) in three ways:
1) A global variable, $HTTP::Message::MAX_BODY_SIZE to limit the
maximum size of ->decoded_content
2) An accessor, ->max_body_size, which can be set for individual
HTTP::Responses
3) An optional parameter to ->decoded_content, which certainly is the
most preferrable option but requires cooperation from all locations where
->decoded_content is called.
Output the Compress::Raw::Zlib version in case a test fails
We might be fine with version 2.061...
Up our prerequisite to 2.061 for the time being...
Update META.json as well...
Amend changes
* Eliminate use of wantarray() in ->max_body_size
* Eliminate use of vars.pm
Also handle Brotli (de)compression
Reindent to match source
Only run zipbomb tests if we have a recent version of Compress::Raw::Zlib
The Bufsize parameter was introduced in 2.060, so using 2.061 should
be fairly safe here
Remove debugging comments, remove misleading comments
In #181 , https://github.com/libwww-perl/HTTP-Message/pull/181#pullrequestreview-1135225013
Add Changes blurb
... mostly to pacify the gods of CI
Max Maischein authored 7 years ago
Olaf Alders committed 1 year, 7 months ago
0 | 0 | Revision history for HTTP-Message |
1 | 1 | |
2 | 2 | {{$NEXT}} |
3 | - Add maximum size for HTTP::Message->decoded_content | |
4 | This can be used to limit the size of a decompressed HTTP response, | |
5 | especially when making requests to untrusted or user-specified servers. | |
6 | The $HTTP::Message::MAXIMUM_BODY_SIZE variable and the ->max_body_size | |
7 | accessor can set this limit. (GH#181) (Max Maischein) | |
3 | 8 | |
4 | 9 | 6.40 2022-10-12 15:45:52Z |
5 | 10 | - Fixed two typos in the doc, originally reported by FatherC |
60 | 60 | "IO::Compress::Bzip2" : "2.021", |
61 | 61 | "IO::Compress::Deflate" : "0", |
62 | 62 | "IO::Compress::Gzip" : "0", |
63 | "Compress::Raw::Zlib" : "2.061", | |
63 | 64 | "IO::HTML" : "0", |
64 | 65 | "IO::Uncompress::Bunzip2" : "2.021", |
65 | 66 | "IO::Uncompress::Gunzip" : "0", |
31 | 31 | "IO::Uncompress::Gunzip" => 0, |
32 | 32 | "IO::Uncompress::Inflate" => 0, |
33 | 33 | "IO::Uncompress::RawInflate" => 0, |
34 | "Compress::Raw::Zlib" => 2.061, | |
34 | 35 | "LWP::MediaTypes" => 6, |
35 | 36 | "MIME::Base64" => "2.1", |
36 | 37 | "MIME::QuotedPrint" => 0, |
6 | 6 | |
7 | 7 | require HTTP::Headers; |
8 | 8 | require Carp; |
9 | ||
10 | our $MAXIMUM_BODY_SIZE; | |
9 | 11 | |
10 | 12 | my $CRLF = "\015\012"; # "\r\n" is not portable |
11 | 13 | unless ($HTTP::URI_CLASS) { |
52 | 54 | bless { |
53 | 55 | '_headers' => $header, |
54 | 56 | '_content' => $content, |
57 | '_max_body_size' => $HTTP::Message::MAXIMUM_BODY_SIZE, | |
55 | 58 | }, $class; |
56 | 59 | } |
57 | ||
58 | 60 | |
59 | 61 | sub parse |
60 | 62 | { |
276 | 278 | return undef; |
277 | 279 | } |
278 | 280 | |
281 | sub max_body_size { | |
282 | my $self = $_[0]; | |
283 | my $old = $self->{_max_body_size}; | |
284 | $self->_set_max_body_size($_[1]) if @_ > 1; | |
285 | return $old; | |
286 | } | |
287 | ||
288 | sub _set_max_body_size { | |
289 | my $self = $_[0]; | |
290 | $self->{_max_body_size} = $_[1]; | |
291 | } | |
279 | 292 | |
280 | 293 | sub decoded_content |
281 | 294 | { |
287 | 300 | $content_ref = $self->content_ref; |
288 | 301 | die "Can't decode ref content" if ref($content_ref) ne "SCALAR"; |
289 | 302 | |
303 | my $content_limit = exists $opt{ max_body_size } ? $opt{ max_body_size } | |
304 | : defined $self->max_body_size ? $self->max_body_size | |
305 | : undef | |
306 | ; | |
307 | my %limiter_options; | |
308 | if( defined $content_limit ) { | |
309 | %limiter_options = (LimitOutput => 1, Bufsize => $content_limit); | |
310 | }; | |
290 | 311 | if (my $h = $self->header("Content-Encoding")) { |
291 | 312 | $h =~ s/^\s+//; |
292 | 313 | $h =~ s/\s+$//; |
294 | 315 | next unless $ce; |
295 | 316 | next if $ce eq "identity" || $ce eq "none"; |
296 | 317 | if ($ce eq "gzip" || $ce eq "x-gzip") { |
297 | require IO::Uncompress::Gunzip; | |
298 | my $output; | |
299 | IO::Uncompress::Gunzip::gunzip($content_ref, \$output, Transparent => 0) | |
300 | or die "Can't gunzip content: $IO::Uncompress::Gunzip::GunzipError"; | |
318 | require Compress::Raw::Zlib; # 'WANT_GZIP_OR_ZLIB', 'Z_BUF_ERROR'; | |
319 | ||
320 | if( ! $content_ref_iscopy and keys %limiter_options) { | |
321 | # Create a copy of the input because Zlib will overwrite it | |
322 | # :-( | |
323 | my $input = "$$content_ref"; | |
324 | $content_ref = \$input; | |
325 | $content_ref_iscopy++; | |
326 | }; | |
327 | my ($i, $status) = Compress::Raw::Zlib::Inflate->new( | |
328 | %limiter_options, | |
329 | ConsumeInput => 0, # overridden by Zlib if we have %limiter_options :-( | |
330 | WindowBits => Compress::Raw::Zlib::WANT_GZIP_OR_ZLIB(), | |
331 | ); | |
332 | my $res = $i->inflate( $content_ref, \my $output ); | |
333 | $res == Compress::Raw::Zlib::Z_BUF_ERROR() | |
334 | and Carp::croak("Decoded content would be larger than $content_limit octets"); | |
335 | $res == Compress::Raw::Zlib::Z_OK() | |
336 | or $res == Compress::Raw::Zlib::Z_STREAM_END() | |
337 | or die "Can't gunzip content: $res"; | |
301 | 338 | $content_ref = \$output; |
302 | 339 | $content_ref_iscopy++; |
303 | 340 | } |
304 | 341 | elsif ($ce eq 'br') { |
305 | 342 | require IO::Uncompress::Brotli; |
306 | 343 | my $bro = IO::Uncompress::Brotli->create; |
307 | my $output = eval { $bro->decompress($$content_ref) }; | |
344 | ||
345 | my $output; | |
346 | if( defined $content_limit ) { | |
347 | $output = eval { $bro->decompress( $$content_ref, $content_limit ); } | |
348 | } else { | |
349 | $output = eval { $bro->decompress($$content_ref) }; | |
350 | } | |
351 | ||
308 | 352 | $@ and die "Can't unbrotli content: $@"; |
309 | 353 | $content_ref = \$output; |
310 | 354 | $content_ref_iscopy++; |
311 | 355 | } |
312 | 356 | elsif ($ce eq "x-bzip2" or $ce eq "bzip2") { |
313 | require IO::Uncompress::Bunzip2; | |
357 | require Compress::Raw::Bzip2; | |
358 | ||
359 | if( ! $content_ref_iscopy ) { | |
360 | # Create a copy of the input because Bzlib2 will overwrite it | |
361 | # :-( | |
362 | my $input = "$$content_ref"; | |
363 | $content_ref = \$input; | |
364 | $content_ref_iscopy++; | |
365 | }; | |
366 | my ($i, $status) = Compress::Raw::Bunzip2->new( | |
367 | 1, # appendInput | |
368 | 0, # consumeInput | |
369 | 0, # small | |
370 | $limiter_options{ LimitOutput } || 0, | |
371 | ); | |
314 | 372 | my $output; |
315 | IO::Uncompress::Bunzip2::bunzip2($content_ref, \$output, Transparent => 0) | |
316 | or die "Can't bunzip content: $IO::Uncompress::Bunzip2::Bunzip2Error"; | |
317 | $content_ref = \$output; | |
373 | $output = "\0" x $limiter_options{ Bufsize } | |
374 | if $limiter_options{ Bufsize }; | |
375 | my $res = $i->bzinflate( $content_ref, \$output ); | |
376 | $res == Compress::Raw::Bzip2::BZ_OUTBUFF_FULL() | |
377 | and Carp::croak("Decoded content would be larger than $content_limit octets"); | |
378 | $res == Compress::Raw::Bzip2::BZ_OK() | |
379 | or $res == Compress::Raw::Bzip2::BZ_STREAM_END() | |
380 | or die "Can't bunzip content: $res"; | |
381 | $content_ref = \$output; | |
318 | 382 | $content_ref_iscopy++; |
319 | 383 | } |
320 | 384 | elsif ($ce eq "deflate") { |
0 | # https://rt.cpan.org/Public/Bug/Display.html?id=52572 | |
1 | ||
2 | use strict; | |
3 | use warnings; | |
4 | ||
5 | use Test::More; | |
6 | ||
7 | use HTTP::Headers qw(); | |
8 | use HTTP::Response qw(); | |
9 | ||
10 | my $ok = eval { | |
11 | require IO::Compress::Brotli; | |
12 | require IO::Uncompress::Brotli; | |
13 | 1; | |
14 | }; | |
15 | if(! $ok) { | |
16 | plan skip_all => "IO::Compress::Brotli needed; $@"; | |
17 | exit | |
18 | } | |
19 | plan tests => 9; | |
20 | ||
21 | # Create a nasty brotli stream: | |
22 | my $size = 16 * 1024 * 1024; | |
23 | my $stream = "\0" x $size; | |
24 | ||
25 | # Compress that stream one time (since it won't compress it twice?!): | |
26 | my $compressed = $stream; | |
27 | my $bro = IO::Compress::Brotli->create; | |
28 | ||
29 | for( 1 ) { | |
30 | my $last = $compressed; | |
31 | $compressed = $bro->compress( $compressed ); | |
32 | $compressed .= $bro->finish(); | |
33 | note sprintf "Encoded size %d bytes after round %d", length $compressed, $_; | |
34 | }; | |
35 | ||
36 | my $body = $compressed; | |
37 | ||
38 | my $headers = HTTP::Headers->new( | |
39 | Content_Type => "application/xml", | |
40 | Content_Encoding => 'br', # only one round needed for Brotli | |
41 | ); | |
42 | my $response = HTTP::Response->new(200, "OK", $headers, $body); | |
43 | ||
44 | my $len = length $response->decoded_content; | |
45 | is($len, 16 * 1024 * 1024, "Self-test: The decoded content length is 16M as expected" ); | |
46 | ||
47 | # Manual decompression check | |
48 | my $output = $compressed; | |
49 | for( 1 ) { | |
50 | my $unbro = IO::Uncompress::Brotli->create(); | |
51 | $output = $unbro->decompress($compressed); | |
52 | }; | |
53 | ||
54 | $headers = HTTP::Headers->new( | |
55 | Content_Type => "application/xml", | |
56 | Content_Encoding => 'br' # say my name, but only once | |
57 | ); | |
58 | ||
59 | $HTTP::Message::MAXIMUM_BODY_SIZE = 1024 * 1024; | |
60 | ||
61 | $response = HTTP::Response->new(200, "OK", $headers, $body); | |
62 | is $response->max_body_size, 1024*1024, "The default maximum body size holds"; | |
63 | ||
64 | $response->max_body_size( 512*1024 ); | |
65 | is $response->max_body_size, 512*1024, "We can change the maximum body size"; | |
66 | ||
67 | my $content; | |
68 | my $lives = eval { | |
69 | $content = $response->decoded_content( raise_error => 1 ); | |
70 | 1; | |
71 | }; | |
72 | my $err = $@; | |
73 | is $lives, undef, "We die when trying to decode something larger than our global limit of 512k" | |
74 | or diag "... using IO::Uncompress::Brotli version $IO::Uncompress::Brotli::VERSION"; | |
75 | ||
76 | $response->max_body_size(undef); | |
77 | is $response->max_body_size, undef, "We can remove the maximum size restriction"; | |
78 | $lives = eval { | |
79 | $content = $response->decoded_content( raise_error => 0 ); | |
80 | 1; | |
81 | }; | |
82 | is $lives, 1, "We don't die when trying to decode something larger than our global limit of 1M"; | |
83 | is length $content, 16 * 1024*1024, "We get the full content"; | |
84 | is $content, $stream, "We really get the full content"; | |
85 | ||
86 | # The best usage of ->decoded_content: | |
87 | $lives = eval { | |
88 | $content = $response->decoded_content( | |
89 | raise_error => 1, | |
90 | max_body_size => 512 * 1024 ); | |
91 | 1; | |
92 | }; | |
93 | $err = $@; | |
94 | is $lives, undef, "We die when trying to decode something larger than our limit of 512k using a parameter" | |
95 | or diag "... using IO::Uncompress::Brotli version $IO::Uncompress::Brotli::VERSION"; | |
96 | ||
97 | =head1 SEE ALSO | |
98 | ||
99 | L<https://security.stackexchange.com/questions/51071/zlib-deflate-decompression-bomb> | |
100 | ||
101 | L<http://www.aerasec.de/security/advisories/decompression-bomb-vulnerability.html> | |
102 | ||
103 | =cut |
0 | # https://rt.cpan.org/Public/Bug/Display.html?id=52572 | |
1 | ||
2 | use strict; | |
3 | use warnings; | |
4 | ||
5 | use Test::More; | |
6 | plan tests => 10; | |
7 | ||
8 | use HTTP::Headers qw( ); | |
9 | use HTTP::Response qw( ); | |
10 | ||
11 | # Create a nasty bzip2 stream: | |
12 | my $size = 16 * 1024 * 1024; | |
13 | my $stream = "\0" x $size; | |
14 | ||
15 | # Compress that stream three times: | |
16 | my $compressed = $stream; | |
17 | for( 1..3 ) { | |
18 | require IO::Compress::Bzip2; | |
19 | my $last = $compressed; | |
20 | IO::Compress::Bzip2::bzip2(\$last, \$compressed) | |
21 | or die "Can't bzip2 content: $IO::Compress::Bzip2::Bzip2Error"; | |
22 | #diag sprintf "Encoded size %d bytes after round %d", length $compressed, $_; | |
23 | }; | |
24 | ||
25 | my $body = $compressed; | |
26 | ||
27 | my $headers = HTTP::Headers->new( | |
28 | Content_Type => "application/xml", | |
29 | Content_Encoding => 'bzip2,bzip2,bzip2', # say my name three times | |
30 | ); | |
31 | my $response = HTTP::Response->new(200, "OK", $headers, $body); | |
32 | ||
33 | my $len = length $response->decoded_content( raise_error => 1 ); | |
34 | is($len, 16 * 1024 * 1024, "Self-test: The decoded content length is 16M as expected" ); | |
35 | ||
36 | # Manual decompression check | |
37 | my $output = $compressed; | |
38 | for( 1..3 ) { | |
39 | my $last = $output; | |
40 | require Compress::Raw::Bzip2; | |
41 | my ($i, $status) = Compress::Raw::Bunzip2->new( | |
42 | 1, # appendInput | |
43 | 0, # consumeInput | |
44 | 0, # small | |
45 | 1, | |
46 | ); | |
47 | $output = "\0" x (1024*1024); | |
48 | # Will modify $last, but we made a copy above | |
49 | my $res = $i->bzinflate( \$last, \$output ); | |
50 | }; | |
51 | is length $output, 1024*1024, "We manually recreate the limited original stream"; | |
52 | ||
53 | $headers = HTTP::Headers->new( | |
54 | Content_Type => "application/xml", | |
55 | Content_Encoding => 'bzip2,bzip2,bzip2', # say my name three times | |
56 | ); | |
57 | ||
58 | $HTTP::Message::MAXIMUM_BODY_SIZE = 1024 * 1024; | |
59 | ||
60 | $response = HTTP::Response->new(200, "OK", $headers, $body); | |
61 | is $response->max_body_size, 1024*1024, "The default maximum body size holds"; | |
62 | ||
63 | $response->max_body_size( 512*1024 ); | |
64 | is $response->max_body_size, 512*1024, "We can change the maximum body size"; | |
65 | ||
66 | my $content; | |
67 | my $lives = eval { | |
68 | $content = $response->decoded_content( raise_error => 1 ); | |
69 | 1; | |
70 | }; | |
71 | my $err = $@; | |
72 | is $lives, undef, "We die when trying to decode something larger than our limit of 512k"; | |
73 | ||
74 | $response->max_body_size(undef); | |
75 | is $response->max_body_size, undef, "We can remove the maximum size restriction"; | |
76 | $lives = eval { | |
77 | $content = $response->decoded_content( raise_error => 0 ); | |
78 | 1; | |
79 | }; | |
80 | is $lives, 1, "We don't die when trying to decode something larger than our global limit of 1M"; | |
81 | is length $content, 16 * 1024*1024, "We get the full content"; | |
82 | is $content, $stream, "We really get the full content"; | |
83 | ||
84 | # The best usage of ->decoded_content: | |
85 | $lives = eval { | |
86 | $content = $response->decoded_content( | |
87 | raise_error => 1, | |
88 | max_body_size => 512 * 1024 ); | |
89 | 1; | |
90 | }; | |
91 | $err = $@; | |
92 | is $lives, undef, "We die when trying to decode something larger than our limit of 512k"; | |
93 | ||
94 | =head1 SEE ALSO | |
95 | ||
96 | L<https://security.stackexchange.com/questions/51071/zlib-deflate-decompression-bomb> | |
97 | ||
98 | L<http://www.aerasec.de/security/advisories/decompression-bomb-vulnerability.html> | |
99 | ||
100 | =cut⏎ |
0 | # https://rt.cpan.org/Public/Bug/Display.html?id=52572 | |
1 | ||
2 | use strict; | |
3 | use warnings; | |
4 | ||
5 | use Test::More; | |
6 | ||
7 | use HTTP::Headers qw( ); | |
8 | use HTTP::Response qw( ); | |
9 | ||
10 | my $ok = eval { | |
11 | require Compress::Raw::Zlib; | |
12 | Compress::Raw::Zlib->VERSION('2.061'); | |
13 | 1; | |
14 | }; | |
15 | if(! $ok) { | |
16 | plan skip_all => "Compress::Raw::Zlib 2.061+ needed; $@"; | |
17 | exit | |
18 | } | |
19 | plan tests => 9; | |
20 | ||
21 | # Create a nasty gzip stream: | |
22 | my $size = 16 * 1024 * 1024; | |
23 | my $stream = "\0" x $size; | |
24 | ||
25 | # Compress that stream three times: | |
26 | my $compressed = $stream; | |
27 | for( 1..3 ) { | |
28 | require IO::Compress::Gzip; | |
29 | my $last = $compressed; | |
30 | IO::Compress::Gzip::gzip(\$last, \$compressed, Level => 9, -Minimal => 1) | |
31 | or die "Can't gzip content: $IO::Compress::Gzip::GzipError"; | |
32 | #diag sprintf "Encoded size %d bytes after round %d", length $compressed, $_; | |
33 | }; | |
34 | ||
35 | my $body = $compressed; | |
36 | ||
37 | my $headers = HTTP::Headers->new( | |
38 | Content_Type => "application/xml", | |
39 | Content_Encoding => 'gzip,gzip,gzip', # say my name three times | |
40 | ); | |
41 | my $response = HTTP::Response->new(200, "OK", $headers, $body); | |
42 | ||
43 | my $len = length $response->decoded_content; | |
44 | is($len, 16 * 1024 * 1024, "Self-test: The decoded content length is 16M as expected" ); | |
45 | ||
46 | # Manual decompression check | |
47 | my $output = $compressed; | |
48 | for( 1..3 ) { | |
49 | use Compress::Raw::Zlib 'WANT_GZIP_OR_ZLIB', 'Z_BUF_ERROR'; | |
50 | ||
51 | my $last = $output; | |
52 | require Compress::Raw::Zlib; | |
53 | my ($i, $status) = Compress::Raw::Zlib::Inflate->new( | |
54 | Bufsize => 1024*1024, | |
55 | LimitOutput => 1, | |
56 | WindowBits => WANT_GZIP_OR_ZLIB | |
57 | ); | |
58 | $output = ''; | |
59 | # Will modify $last, but we made a copy above | |
60 | my $res = $i->inflate( \$last, \$output ); | |
61 | }; | |
62 | ||
63 | $headers = HTTP::Headers->new( | |
64 | Content_Type => "application/xml", | |
65 | Content_Encoding => 'gzip, gzip, gzip' # say my name three times | |
66 | ); | |
67 | ||
68 | $HTTP::Message::MAXIMUM_BODY_SIZE = 1024 * 1024; | |
69 | ||
70 | $response = HTTP::Response->new(200, "OK", $headers, $body); | |
71 | is $response->max_body_size, 1024*1024, "The default maximum body size holds"; | |
72 | ||
73 | $response->max_body_size( 512*1024 ); | |
74 | is $response->max_body_size, 512*1024, "We can change the maximum body size"; | |
75 | ||
76 | my $content; | |
77 | my $lives = eval { | |
78 | $content = $response->decoded_content( raise_error => 1 ); | |
79 | 1; | |
80 | }; | |
81 | my $err = $@; | |
82 | is $lives, undef, "We die when trying to decode something larger than our global limit of 512k" | |
83 | or diag "... using Compress::Raw::Zlib version $Compress::Raw::Zlib::VERSION"; | |
84 | ||
85 | $response->max_body_size(undef); | |
86 | is $response->max_body_size, undef, "We can remove the maximum size restriction"; | |
87 | $lives = eval { | |
88 | $content = $response->decoded_content( raise_error => 0 ); | |
89 | 1; | |
90 | }; | |
91 | is $lives, 1, "We don't die when trying to decode something larger than our global limit of 1M"; | |
92 | is length $content, 16 * 1024*1024, "We get the full content"; | |
93 | is $content, $stream, "We really get the full content"; | |
94 | ||
95 | # The best usage of ->decoded_content: | |
96 | $lives = eval { | |
97 | $content = $response->decoded_content( | |
98 | raise_error => 1, | |
99 | max_body_size => 512 * 1024 ); | |
100 | 1; | |
101 | }; | |
102 | $err = $@; | |
103 | is $lives, undef, "We die when trying to decode something larger than our limit of 512k using a parameter" | |
104 | or diag "... using Compress::Raw::Zlib version $Compress::Raw::Zlib::VERSION"; | |
105 | ||
106 | =head1 SEE ALSO | |
107 | ||
108 | L<https://security.stackexchange.com/questions/51071/zlib-deflate-decompression-bomb> | |
109 | ||
110 | L<http://www.aerasec.de/security/advisories/decompression-bomb-vulnerability.html> | |
111 | ||
112 | =cut |