Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package perl-HTTP-Body for openSUSE:Factory
checked in at 2026-11-06 19:59:10
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/perl-HTTP-Body (Old)
and /work/SRC/openSUSE:Factory/.perl-HTTP-Body.new.3759290 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "perl-HTTP-Body"
Fri Nov 6 19:59:10 2026 rev:18 rq:1395878 version:1.250.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/perl-HTTP-Body/perl-HTTP-Body.changes
2026-10-29 16:10:28.294456721 +0100
+++
/work/SRC/openSUSE:Factory/.perl-HTTP-Body.new.3759290/perl-HTTP-Body.changes
2026-11-06 20:07:34.209365340 +0100
@@ -1,0 +2,46 @@
+Fri Nov 6 11:31:37 UTC 2026 - Tina Müller <[email protected]>
+
+- updated to 1.250.0 (1.25)
+ see /usr/share/doc/packages/perl-HTTP-Body/Changes
+
+ 1.25 2026-11-05 18:47:08+01:00 Europe/Berlin
+ - SECURITY, CVE-2026-117500: the suffix of an upload's filename is
+ worked out from its last 255 bytes only, only when $file_temp_suffix
+ is unset, and for the default $basename_regexp by a string search
+ instead of the regexp; a filename of many dots ending in a backslash
+ made the default $basename_regexp take time quadratic in its length
+ (7 s per part at the 32 KiB header limit, once per part) in the
+ default configuration
+ bsc#1288651
+ - SECURITY, CVE-2026-117501: a chunked body is framed line by line with
+ a limit on the chunk-size line ($HTTP::Body::max_chunk_line_size,
+ 1024 bytes), chunk data is passed on as it arrives instead of being
+ held until the chunk is complete, malformed framing (a line that is
+ no chunk-size line, a size that does not fit an integer, chunk data
+ not followed by CRLF) croaks instead of buffering forever, and
+ nothing after the trailer or after the body parser is done is kept
+ bsc#1288652
+ - SECURITY, CVE-2026-121006: the trailer of a chunked body is limited
+ to $HTTP::Body::max_trailer_size bytes (32768) and
+ $HTTP::Body::max_trailer_lines header lines (100); everything that
+ followed the last chunk in the same call to add() was parsed with
+ HTTP::Headers, which takes time quadratic in the number of fields of
+ one name (6 s for 16 KiB of "a:" lines)
+ bsc#1289589
+ - trailing_headers: a trailer is read up to its blank line also when it
+ arrives in later calls to add() than the last chunk; it used to be
+ lost
+ - XFormsMultipart: a part other than the start part now gets its
+ temporary file and the NUL strip of RT#123615 like any upload; it used
+ to be returned by upload without a tempname, its data dropped
+ - parse_header: a continuation line at the start of a part's header
+ block is dropped instead of dying, and a line that is no header field
+ is dropped instead of being stored under a made-up field name
+ - add: the caller's buffer is no longer modified in place when it
+ exceeds the Content-Length, so a read-only value is accepted
+ - Docs: every method of the parser classes is described, add and
+ length say what they return; the README lists every limit,
+ $basename_regexp and all runtime prerequisites
+ - File::Spec is declared as a prerequisite
+
+-------------------------------------------------------------------
Old:
----
HTTP-Body-1.24.tar.gz
New:
----
HTTP-Body-1.25.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ perl-HTTP-Body.spec ++++++
--- /var/tmp/diff_new_pack.vvjcaQ/_old 2026-11-06 20:07:35.214407459 +0100
+++ /var/tmp/diff_new_pack.vvjcaQ/_new 2026-11-06 20:07:35.215407500 +0100
@@ -18,10 +18,10 @@
%define cpan_name HTTP-Body
Name: perl-HTTP-Body
-Version: 1.240.0
+Version: 1.250.0
Release: 0
-# 1.24 -> normalize -> 1.240.0
-%define cpan_version 1.24
+# 1.25 -> normalize -> 1.250.0
+%define cpan_version 1.25
License: Artistic-1.0 OR GPL-1.0-or-later
Summary: HTTP Body Parser
URL: https://metacpan.org/release/%{cpan_name}
@@ -49,7 +49,9 @@
%description
HTTP::Body parses chunks of HTTP POST data and supports
application/octet-stream, application/json,
-application/x-www-form-urlencoded, and multipart/form-data.
+application/x-www-form-urlencoded, multipart/form-data, and XForms
+submissions (multipart/related and application/xml). A body of any other
+content type is handled like application/octet-stream.
Chunked bodies are supported by not passing a length value to new().
++++++ HTTP-Body-1.24.tar.gz -> HTTP-Body-1.25.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/Changes new/HTTP-Body-1.25/Changes
--- old/HTTP-Body-1.24/Changes 2026-10-26 18:28:04.000000000 +0100
+++ new/HTTP-Body-1.25/Changes 2026-11-05 18:47:11.000000000 +0100
@@ -1,5 +1,42 @@
This file documents the revision history for Perl extension HTTP::Body.
+1.25 2026-11-05 18:47:08+01:00 Europe/Berlin
+ - SECURITY, CVE-2026-117500: the suffix of an upload's filename is
+ worked out from its last 255 bytes only, only when $file_temp_suffix
+ is unset, and for the default $basename_regexp by a string search
+ instead of the regexp; a filename of many dots ending in a backslash
+ made the default $basename_regexp take time quadratic in its length
+ (7 s per part at the 32 KiB header limit, once per part) in the
+ default configuration
+ - SECURITY, CVE-2026-117501: a chunked body is framed line by line with
+ a limit on the chunk-size line ($HTTP::Body::max_chunk_line_size,
+ 1024 bytes), chunk data is passed on as it arrives instead of being
+ held until the chunk is complete, malformed framing (a line that is
+ no chunk-size line, a size that does not fit an integer, chunk data
+ not followed by CRLF) croaks instead of buffering forever, and
+ nothing after the trailer or after the body parser is done is kept
+ - SECURITY, CVE-2026-121006: the trailer of a chunked body is limited
+ to $HTTP::Body::max_trailer_size bytes (32768) and
+ $HTTP::Body::max_trailer_lines header lines (100); everything that
+ followed the last chunk in the same call to add() was parsed with
+ HTTP::Headers, which takes time quadratic in the number of fields of
+ one name (6 s for 16 KiB of "a:" lines)
+ - trailing_headers: a trailer is read up to its blank line also when it
+ arrives in later calls to add() than the last chunk; it used to be
+ lost
+ - XFormsMultipart: a part other than the start part now gets its
+ temporary file and the NUL strip of RT#123615 like any upload; it used
+ to be returned by upload without a tempname, its data dropped
+ - parse_header: a continuation line at the start of a part's header
+ block is dropped instead of dying, and a line that is no header field
+ is dropped instead of being stored under a made-up field name
+ - add: the caller's buffer is no longer modified in place when it
+ exceeds the Content-Length, so a read-only value is accepted
+ - Docs: every method of the parser classes is described, add and
+ length say what they return; the README lists every limit,
+ $basename_regexp and all runtime prerequisites
+ - File::Spec is declared as a prerequisite
+
1.24 2026-10-26 18:28:01+01:00 Europe/Berlin
- SECURITY, CVE-2026-116131: urlencoded bodies are limited to
$HTTP::Body::UrlEncoded::max_params fields and multipart bodies to
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/MANIFEST new/HTTP-Body-1.25/MANIFEST
--- old/HTTP-Body-1.24/MANIFEST 2026-10-26 18:28:04.000000000 +0100
+++ new/HTTP-Body-1.25/MANIFEST 2026-11-05 18:47:11.000000000 +0100
@@ -33,6 +33,13 @@
t/18chunked-size-line.t
t/19chunked-split.t
t/20temp-write-and-filename.t
+t/21filename-suffix-redos.t
+t/22chunked-malformed.t
+t/23xforms-attachment.t
+t/24part-header-lines.t
+t/25add-keeps-argument.t
+t/26filename-suffix-string-search.t
+t/27chunked-trailer.t
t/data/multipart/001-content.dat
t/data/multipart/001-headers.pml
t/data/multipart/001-results.pml
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/META.json new/HTTP-Body-1.25/META.json
--- old/HTTP-Body-1.24/META.json 2026-10-26 18:28:04.000000000 +0100
+++ new/HTTP-Body-1.25/META.json 2026-11-05 18:47:11.000000000 +0100
@@ -25,6 +25,7 @@
"runtime" : {
"requires" : {
"Carp" : "0",
+ "File::Spec" : "0",
"File::Temp" : "0.14",
"HTTP::Message" : "0",
"IO::File" : "1.14"
@@ -53,7 +54,7 @@
"web" : "https://github.com/libwww-perl/HTTP-Body"
}
},
- "version" : "1.24",
+ "version" : "1.25",
"x_generated_by_perl" : "v5.40.1",
"x_serialization_backend" : "Cpanel::JSON::XS version 4.53",
"x_spdx_expression" : "Artistic-1.0-Perl OR GPL-1.0-or-later"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/META.yml new/HTTP-Body-1.25/META.yml
--- old/HTTP-Body-1.24/META.yml 2026-10-26 18:28:04.000000000 +0100
+++ new/HTTP-Body-1.25/META.yml 2026-11-05 18:47:11.000000000 +0100
@@ -23,6 +23,7 @@
name: HTTP-Body
requires:
Carp: '0'
+ File::Spec: '0'
File::Temp: '0.14'
HTTP::Message: '0'
IO::File: '1.14'
@@ -30,7 +31,7 @@
bugtracker: https://github.com/libwww-perl/HTTP-Body/issues
homepage: https://metacpan.org/release/HTTP-Body
repository: https://github.com/libwww-perl/HTTP-Body.git
-version: '1.24'
+version: '1.25'
x_generated_by_perl: v5.40.1
x_serialization_backend: 'YAML::Tiny version 1.76'
x_spdx_expression: 'Artistic-1.0-Perl OR GPL-1.0-or-later'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/Makefile.PL
new/HTTP-Body-1.25/Makefile.PL
--- old/HTTP-Body-1.24/Makefile.PL 2026-10-26 18:28:04.000000000 +0100
+++ new/HTTP-Body-1.25/Makefile.PL 2026-11-05 18:47:11.000000000 +0100
@@ -17,6 +17,7 @@
"NAME" => "HTTP::Body",
"PREREQ_PM" => {
"Carp" => 0,
+ "File::Spec" => 0,
"File::Temp" => "0.14",
"HTTP::Message" => 0,
"IO::File" => "1.14"
@@ -29,7 +30,7 @@
"Test::Deep" => 0,
"Test::More" => "0.86"
},
- "VERSION" => "1.24",
+ "VERSION" => "1.25",
"test" => {
"TESTS" => "t/*.t"
}
@@ -40,6 +41,7 @@
"Carp" => 0,
"Digest::MD5" => 0,
"Encode" => 0,
+ "File::Spec" => 0,
"File::Spec::Functions" => 0,
"File::Temp" => "0.14",
"HTTP::Message" => 0,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/README new/HTTP-Body-1.25/README
--- old/HTTP-Body-1.24/README 2026-10-26 18:28:04.000000000 +0100
+++ new/HTTP-Body-1.25/README 2026-11-05 18:47:11.000000000 +0100
@@ -1,5 +1,5 @@
This archive contains the distribution HTTP-Body,
-version 1.24:
+version 1.25:
HTTP Body Parser
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/README.md new/HTTP-Body-1.25/README.md
--- old/HTTP-Body-1.24/README.md 2026-10-26 18:28:04.000000000 +0100
+++ new/HTTP-Body-1.25/README.md 2026-11-05 18:47:11.000000000 +0100
@@ -54,11 +54,13 @@
### Dependencies
-- [HTTP::Headers](https://metacpan.org/pod/HTTP::Headers)
-- [HTTP::Message](https://metacpan.org/pod/HTTP::Message)
+- [HTTP::Message](https://metacpan.org/pod/HTTP::Message) (includes
[HTTP::Headers](https://metacpan.org/pod/HTTP::Headers))
- [File::Temp](https://metacpan.org/pod/File::Temp) (>= 0.14)
- [IO::File](https://metacpan.org/pod/IO::File) (>= 1.14)
-- [Digest::MD5](https://metacpan.org/pod/Digest::MD5)
+- [File::Spec](https://metacpan.org/pod/File::Spec)
+- [Carp](https://metacpan.org/pod/Carp)
+
+All but HTTP::Message ship with Perl itself.
## Synopsis
@@ -142,6 +144,11 @@
| `$HTTP::Body::UrlEncoded::max_params` | 1000 | fields in a urlencoded body |
| `$HTTP::Body::MultiPart::max_header_size` | 32768 | bytes in a part's header
block |
| `$HTTP::Body::MultiPart::max_header_lines` | 100 | lines in a part's header
block |
+| `$HTTP::Body::max_chunk_line_size` | 1024 | bytes in a chunk-size line of a
chunked body |
+| `$HTTP::Body::max_trailer_size` | 32768 | bytes in the trailer of a chunked
body |
+| `$HTTP::Body::max_trailer_lines` | 100 | header lines in the trailer of a
chunked body |
+
+A chunked body with malformed framing (a line that is no chunk-size line, a
size that does not fit an integer, chunk data not followed by CRLF) makes
`add()` croak as well.
## Configuring Upload Temporary Files
@@ -151,6 +158,10 @@
# Keep the uploaded file's own extension (behavior before 1.23)
$HTTP::Body::MultiPart::file_temp_suffix = undef;
+# How that extension is taken from the filename (only used when
+# file_temp_suffix is undef; matched against the last 255 bytes)
+$HTTP::Body::MultiPart::basename_regexp = qr/(\.\w+)$/;
+
# Custom File::Temp template
$HTTP::Body::MultiPart::file_temp_template = 'uploadXXXXXX';
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/dist.ini new/HTTP-Body-1.25/dist.ini
--- old/HTTP-Body-1.24/dist.ini 2026-10-26 18:28:04.000000000 +0100
+++ new/HTTP-Body-1.25/dist.ini 2026-11-05 18:47:11.000000000 +0100
@@ -17,6 +17,7 @@
[Prereqs]
Carp = 0
+File::Spec = 0
File::Temp = 0.14
HTTP::Message = 0
IO::File = 1.14
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/lib/HTTP/Body/MultiPart.pm
new/HTTP-Body-1.25/lib/HTTP/Body/MultiPart.pm
--- old/HTTP-Body-1.24/lib/HTTP/Body/MultiPart.pm 2026-10-26
18:28:04.000000000 +0100
+++ new/HTTP-Body-1.25/lib/HTTP/Body/MultiPart.pm 2026-11-05
18:47:11.000000000 +0100
@@ -1,5 +1,5 @@
package HTTP::Body::MultiPart;
-$HTTP::Body::MultiPart::VERSION = '1.24';
+$HTTP::Body::MultiPart::VERSION = '1.25';
use strict;
use base 'HTTP::Body';
use bytes;
@@ -18,7 +18,16 @@
=head1 DESCRIPTION
-HTTP Body Multipart Parser.
+HTTP Body Multipart Parser for multipart/form-data bodies.
+
+A part with a non-empty filename in its Content-Disposition is an upload: its
+data is written to a temporary file as it arrives, and the part is stored with
+L<HTTP::Body/upload> once it is complete. Every other part is stored with
+L<HTTP::Body/param> and L<HTTP::Body/part_data>, except a part with an empty
+filename (a file field left empty), which is dropped.
+
+See L<HTTP::Body/Limits> for the limits on parts and their headers, and
+L<HTTP::Body/CONFIGURING UPLOAD TEMPORARY FILES> for the temporary files.
=head1 METHODS
@@ -26,6 +35,9 @@
=item init
+Takes the boundary from the content type, croaking if it has none, and starts
+the parser in the preamble state.
+
=cut
sub init {
@@ -44,6 +56,10 @@
=item spin
+Runs the parse method of the current state (parse_preamble, parse_boundary,
+parse_header or parse_body) until one of them needs more data or the body is
+done.
+
=cut
sub spin {
@@ -64,6 +80,8 @@
=item boundary
+The boundary parameter of the content type.
+
=cut
sub boundary {
@@ -72,6 +90,8 @@
=item boundary_begin
+C<--> followed by the boundary.
+
=cut
sub boundary_begin {
@@ -80,6 +100,8 @@
=item boundary_end
+boundary_begin followed by C<-->.
+
=cut
sub boundary_end {
@@ -88,6 +110,8 @@
=item crlf
+The line ending of a multipart body, C<"\x0D\x0A">.
+
=cut
sub crlf () {
@@ -96,6 +120,8 @@
=item delimiter_begin
+crlf followed by boundary_begin: the delimiter that ends the data of a part.
+
=cut
sub delimiter_begin {
@@ -105,6 +131,8 @@
=item delimiter_end
+crlf followed by boundary_end: the close delimiter that ends the body.
+
=cut
sub delimiter_end {
@@ -114,6 +142,10 @@
=item parse_preamble
+Drops the preamble before the first boundary and moves on to the boundary
+state. Until the boundary is found, only as many bytes are kept as could be
+its start.
+
=cut
sub parse_preamble {
@@ -138,6 +170,11 @@
=item parse_boundary
+Reads a delimiter line. A delimiter starts the next part and the header
+state; a close delimiter ends the body, and whatever follows it (padding, an
+epilogue) is ignored. Croaks on a line that is neither, and when the body has
+more than C<$HTTP::Body::MultiPart::max_parts> parts.
+
=cut
our $max_parts = 1000;
@@ -182,6 +219,13 @@
=item parse_header
+Reads a part's header block, up to the blank line, into C<< $part->{headers}
>>,
+keyed by field name with the first letter of each word upper-cased; a field
+that occurs more than once gets an array reference. A continuation line is
+joined to the line before it, and a line that is no header field is dropped.
+Croaks when the block exceeds C<$HTTP::Body::MultiPart::max_header_size> bytes
+or C<$HTTP::Body::MultiPart::max_header_lines> lines.
+
=cut
our $max_header_size = 32 * 1024;
@@ -214,7 +258,8 @@
my @headers;
for ( split /$crlf/, $header ) {
if (s/^[ \t]+//) {
- $headers[-1] .= $_;
+ # a continuation line; with no line to continue it is dropped
+ $headers[-1] .= $_ if @headers;
}
else {
push @headers, $_;
@@ -225,7 +270,9 @@
for my $header (@headers) {
- $header =~ s/^($token):[\t ]*//;
+ # A line that is no header field is dropped: without a match $1 would
+ # still hold whatever the last successful match captured.
+ next unless $header =~ s/^($token):[\t ]*//;
( my $field = $1 ) =~ s/\b(\w)/uc($1)/eg;
@@ -247,6 +294,11 @@
=item parse_body
+Passes a part's data on to handler as it arrives, holding back only as many
+bytes as could be the start of the next delimiter. At the delimiter it sets
+C<< $part->{done} >>, calls handler a last time and moves on to the boundary
+state.
+
=cut
sub parse_body {
@@ -285,9 +337,20 @@
=item handler
+Called by parse_body with the part each time data arrives. On the first call
+it takes the part's name and filename from its Content-Disposition, unless a
+subclass has named the part already, and strips any NUL from the filename. A
+part with a non-empty filename gets a temporary file, and its data is written
+there as it arrives. Once the part is done it is stored as described under
+L</DESCRIPTION>. Croaks when the temporary file can not be written.
+
=cut
-our $basename_regexp = qr/[^.]+(\.[^\\\/]+)$/;
+# The default is also kept here: while $basename_regexp still holds it, the
+# suffix is found by _default_suffix, a string search with the same result.
+my $default_basename_regexp = qr/[^.]+(\.[^\\\/]+)$/;
+
+our $basename_regexp = $default_basename_regexp;
our $file_temp_suffix = '.upload';
our $file_temp_template;
our %file_temp_parameters;
@@ -302,28 +365,46 @@
my ($filename) = $disposition =~ / filename="?([^\"]*)"?/;
# Need to match empty filenames above, so this part is flagged as an
upload type
+ $part->{name} = $name;
+ $part->{filename} = $filename if defined $filename;
+ }
+
+ # A file part gets its temporary file on the first call, also when a
+ # subclass has named the part and set its filename itself.
+ if ( defined $part->{filename} && !exists $part->{tempname} ) {
+
# A NUL in the filename truncates the path for anything downstream that
# hands it to the C layer, so strip it before the name is used or
stored.
- $filename =~ s/\0//g if defined $filename;
-
- $part->{name} = $name;
+ $part->{filename} =~ s/\0//g;
- if ( defined $filename ) {
- $part->{filename} = $filename;
+ if ( $part->{filename} ne "" ) {
+ my $suffix = $file_temp_suffix;
- if ( $filename ne "" ) {
- my $basename = (File::Spec->splitpath($filename))[2];
- my $suffix = $basename =~ $basename_regexp ? $1 : q{};
+ unless ($suffix) {
+ my $basename = (File::Spec->splitpath($part->{filename}))[2];
- my $fh = File::Temp->new(
- UNLINK => 0, DIR => $self->tmpdir, SUFFIX =>
($file_temp_suffix||$suffix),
- ( $file_temp_template ? ( TEMPLATE => $file_temp_template
) : () ),
- %file_temp_parameters,
- );
+ # A suffix can be no longer than a file name, so only the end
+ # of a long basename is looked at. On the whole of a hostile
+ # one a regexp like the default backtracks in time quadratic
+ # in its length: seconds per part, once for every part.
+ $basename = substr( $basename, -255 ) if length($basename) >
255;
- $part->{fh} = $fh;
- $part->{tempname} = $fh->filename;
+ if ( ref $basename_regexp && $basename_regexp ==
$default_basename_regexp ) {
+ $suffix = _default_suffix($basename);
+ }
+ else {
+ $suffix = $basename =~ $basename_regexp ? $1 : q{};
+ }
}
+
+ my $fh = File::Temp->new(
+ UNLINK => 0, DIR => $self->tmpdir, SUFFIX => $suffix,
+ ( $file_temp_template ? ( TEMPLATE => $file_temp_template ) :
() ),
+ %file_temp_parameters,
+ );
+
+ $part->{fh} = $fh;
+ $part->{tempname} = $fh->filename;
}
}
@@ -356,6 +437,27 @@
}
}
+# The suffix the default $basename_regexp captures, found without a regexp,
+# in time linear in the basename's length. It starts at the first dot after
+# the last slash or backslash that follows a byte other than a dot, and runs
+# to the end; a dot that is the last byte gives no suffix.
+sub _default_suffix {
+ my ($basename) = @_;
+
+ my $last = length($basename) - 1;
+ my $dot = rindex( $basename, '/' );
+ my $back = rindex( $basename, '\\' );
+ $dot = $back if $back > $dot;
+
+ while ( ( $dot = index( $basename, '.', $dot + 1 ) ) >= 0 ) {
+ return q{} if $dot == $last;
+ return substr( $basename, $dot )
+ if $dot > 0 && substr( $basename, $dot - 1, 1 ) ne '.';
+ }
+
+ return q{};
+}
+
=back
=head1 SUPPORT
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/lib/HTTP/Body/OctetStream.pm
new/HTTP-Body-1.25/lib/HTTP/Body/OctetStream.pm
--- old/HTTP-Body-1.24/lib/HTTP/Body/OctetStream.pm 2026-10-26
18:28:04.000000000 +0100
+++ new/HTTP-Body-1.25/lib/HTTP/Body/OctetStream.pm 2026-11-05
18:47:11.000000000 +0100
@@ -1,5 +1,5 @@
package HTTP::Body::OctetStream;
-$HTTP::Body::OctetStream::VERSION = '1.24';
+$HTTP::Body::OctetStream::VERSION = '1.25';
use strict;
use base 'HTTP::Body';
use bytes;
@@ -16,7 +16,9 @@
=head1 DESCRIPTION
-HTTP Body OctetStream Parser.
+HTTP Body OctetStream Parser. It stores the body unparsed in a temporary
+file, for application/octet-stream, application/json and any content type
+HTTP::Body has no other parser for.
=head1 METHODS
@@ -24,6 +26,11 @@
=item spin
+Writes the data that has arrived to a temporary file in
+L<tmpdir|HTTP::Body/tmpdir>, which is the L<body|HTTP::Body/body>. Once the
+whole body is there, it seeks the file back to its start and sets the state to
+done. Croaks when the file can not be written.
+
=cut
sub spin {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/lib/HTTP/Body/UrlEncoded.pm
new/HTTP-Body-1.25/lib/HTTP/Body/UrlEncoded.pm
--- old/HTTP-Body-1.24/lib/HTTP/Body/UrlEncoded.pm 2026-10-26
18:28:04.000000000 +0100
+++ new/HTTP-Body-1.25/lib/HTTP/Body/UrlEncoded.pm 2026-11-05
18:47:11.000000000 +0100
@@ -1,5 +1,5 @@
package HTTP::Body::UrlEncoded;
-$HTTP::Body::UrlEncoded::VERSION = '1.24';
+$HTTP::Body::UrlEncoded::VERSION = '1.25';
use strict;
use base 'HTTP::Body';
use bytes;
@@ -25,7 +25,7 @@
=head1 DESCRIPTION
-HTTP Body UrlEncoded Parser.
+HTTP Body UrlEncoded Parser for application/x-www-form-urlencoded bodies.
=head1 METHODS
@@ -33,6 +33,12 @@
=item spin
+Waits until the whole body has arrived, then splits it into fields on C<&>
+and C<;>, decodes C<+> and percent escapes in names and values, and stores
+each field with L<HTTP::Body/param>. A field without C<=> is dropped. Croaks
+when the body has more than C<$HTTP::Body::UrlEncoded::max_params> fields (see
+L<HTTP::Body/Limits>).
+
=cut
sub spin {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/lib/HTTP/Body/XForms.pm
new/HTTP-Body-1.25/lib/HTTP/Body/XForms.pm
--- old/HTTP-Body-1.24/lib/HTTP/Body/XForms.pm 2026-10-26 18:28:04.000000000
+0100
+++ new/HTTP-Body-1.25/lib/HTTP/Body/XForms.pm 2026-11-05 18:47:11.000000000
+0100
@@ -1,5 +1,5 @@
package HTTP::Body::XForms;
-$HTTP::Body::XForms::VERSION = '1.24';
+$HTTP::Body::XForms::VERSION = '1.25';
use strict;
use base 'HTTP::Body';
use bytes;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/lib/HTTP/Body/XFormsMultipart.pm
new/HTTP-Body-1.25/lib/HTTP/Body/XFormsMultipart.pm
--- old/HTTP-Body-1.24/lib/HTTP/Body/XFormsMultipart.pm 2026-10-26
18:28:04.000000000 +0100
+++ new/HTTP-Body-1.25/lib/HTTP/Body/XFormsMultipart.pm 2026-11-05
18:47:11.000000000 +0100
@@ -1,5 +1,5 @@
package HTTP::Body::XFormsMultipart;
-$HTTP::Body::XFormsMultipart::VERSION = '1.24';
+$HTTP::Body::XFormsMultipart::VERSION = '1.25';
use strict;
use base 'HTTP::Body::MultiPart';
use bytes;
@@ -13,7 +13,7 @@
=head1 SYNOPSIS
- use HTTP::Body::XForms;
+ use HTTP::Body::XFormsMultipart;
=head1 DESCRIPTION
@@ -24,6 +24,10 @@
in the content-type. The XML content is stored unparsed on the
parameter XForms:Model.
+Every other part is an upload named after its Content-ID: its data is
+written to a temporary file like that of any multipart upload, and a NUL
+in the Content-ID is stripped.
+
=head1 METHODS
=over 4
@@ -60,8 +64,9 @@
=item handler
-This function is overridden to differ the start part, which should be
-set as the XForms:Model param if its content type is application/xml.
+This function is overridden to name the parts by their Content-ID: the
+start part becomes the XForms:Model param, and its data the body; every
+other part becomes an upload, as described above.
=cut
@@ -71,7 +76,9 @@
my $contentid = $part->{headers}{'Content-ID'};
$contentid =~ s/^.*[\<\"]//;
$contentid =~ s/[\>\"].*$//;
-
+ # It names the part and its file: a NUL would truncate that downstream.
+ $contentid =~ s/\0//g;
+
if ( $contentid eq $self->start ) {
$part->{name} = 'XForms:Model';
if ($part->{done}) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/lib/HTTP/Body.pm
new/HTTP-Body-1.25/lib/HTTP/Body.pm
--- old/HTTP-Body-1.24/lib/HTTP/Body.pm 2026-10-26 18:28:04.000000000 +0100
+++ new/HTTP-Body-1.25/lib/HTTP/Body.pm 2026-11-05 18:47:11.000000000 +0100
@@ -1,5 +1,5 @@
package HTTP::Body;
-$HTTP::Body::VERSION = '1.24';
+$HTTP::Body::VERSION = '1.25';
use strict;
use Carp qw[ ];
@@ -29,13 +29,13 @@
=head1 SYNOPSIS
use HTTP::Body;
-
+
sub handler : method {
my ( $class, $r ) = @_;
my $content_type = $r->headers_in->get('Content-Type');
my $content_length = $r->headers_in->get('Content-Length');
-
+
my $body = HTTP::Body->new( $content_type, $content_length );
my $length = $content_length;
@@ -44,21 +44,23 @@
$r->read( my $buffer, ( $length < 8192 ) ? $length : 8192 );
$length -= length($buffer);
-
+
$body->add($buffer);
}
-
- my $uploads = $body->upload; # hashref
- my $params = $body->param; # hashref
- my $param_order = $body->param_order # arrayref
- my $body = $body->body; # IO::Handle
+
+ my $uploads = $body->upload; # hashref
+ my $params = $body->param; # hashref
+ my $param_order = $body->param_order; # arrayref
+ my $raw = $body->body; # IO::Handle
}
=head1 DESCRIPTION
HTTP::Body parses chunks of HTTP POST data and supports
application/octet-stream, application/json, application/x-www-form-urlencoded,
-and multipart/form-data.
+multipart/form-data, and XForms submissions (multipart/related and
+application/xml). A body of any other content type is handled like
+application/octet-stream.
Chunked bodies are supported by not passing a length value to new().
@@ -96,6 +98,21 @@
The largest header block a multipart part may have, in bytes and in lines; a
larger one makes add croak. The defaults are 32768 and 100.
+=item $HTTP::Body::max_chunk_line_size
+
+The largest chunk-size line (size, extensions and CRLF) of a chunked body, in
+bytes; a longer one makes add croak. The default is 1024. A chunked body
+whose framing is malformed (a line that is no chunk-size line, a size that does
+not fit an integer, chunk data not followed by CRLF) makes add croak as well.
+
+=item $HTTP::Body::max_trailer_size
+
+=item $HTTP::Body::max_trailer_lines
+
+The largest trailer of a chunked body (its header lines and the blank line
+that ends it), in bytes, and the most header lines it may have; a larger one
+makes add croak. The defaults are 32768 and 100.
+
=back
=head1 METHODS
@@ -132,6 +149,11 @@
cleanup => 0,
buffer => '',
chunk_buffer => '',
+ chunk_remaining => 0,
+ chunk_crlf => 0,
+ chunk_eof => 0,
+ chunk_trailer => undef,
+ chunk_trailer_lines => 0,
body => undef,
chunked => !defined $content_length,
content_length => defined $content_length ? $content_length : -1,
@@ -179,95 +201,169 @@
=item add
-Add string to internal buffer. Will call spin unless done. returns
-length before adding self.
+Feeds the next piece of the body to the parser, which parses as much of it as
+it can unless it is done already. Data beyond the content length is ignored.
+
+With a content length, add returns the number of bytes still expected as a
+negative number, and 0 once the body is complete. For a chunked body it
+returns nothing.
+
+add croaks when one of the limits under L</Limits> is exceeded, and when the
+framing of a chunked body is malformed.
=cut
-# chunk-size [ chunk-ext ] CRLF. The atomic group keeps the size digits from
-# backtracking into the rest of the line, which took quadratic time on a line
-# that does not end in CRLF.
-my $chunk_size_line = qr/^(?>([\da-fA-F]+))[^\n]*\x0D\x0A/;
+# chunk-size [ chunk-ext ] CRLF, matched against one line at a time. The
+# atomic group keeps the size digits from backtracking into the rest of the
+# line, and the line length limit bounds what the rest can cost.
+my $chunk_size_line = qr/^(?>([\da-fA-F]+))[^\n]*\x0D\x0A\z/;
+
+our $max_chunk_line_size = 1024;
+our $max_trailer_size = 32 * 1024;
+our $max_trailer_lines = 100;
sub add {
my $self = shift;
if ( $self->{chunked} ) {
- $self->{chunk_buffer} .= $_[0];
- # Stop once the zero-length chunk has ended the chunked stream: any
- # trailing bytes (e.g. trailing headers) must not be reparsed as
- # further chunks. This is separate from the body parser's own state,
- # which for multipart reaches 'done' at the closing boundary, before
- # the zero chunk that records the final content length.
- while ( !$self->{chunk_eof}
- && $self->{chunk_buffer} =~ $chunk_size_line ) {
- my $chunk_len = hex($1);
- # Length of the chunk-size line itself; the pattern is anchored at
^.
- my $header_len = $+[0];
+ # Nothing after the blank line that ends the trailer belongs to the
+ # body: it must neither be reparsed nor pile up in the buffer.
+ return if $self->{chunk_eof};
+
+ $self->{chunk_buffer} .= $_[0] if defined $_[0];
+
+ while ( !$self->{chunk_eof} ) {
+
+ # Inside a chunk: pass its data on as it arrives, so that a chunk
+ # never needs as much memory as it is long.
+ if ( $self->{chunk_remaining} ) {
+ my $take = length $self->{chunk_buffer};
+ $take = $self->{chunk_remaining} if $take >
$self->{chunk_remaining};
+
+ # wait for more calls to add()
+ return unless $take;
+
+ my $data = substr( $self->{chunk_buffer}, 0, $take, '' );
+ $self->{chunk_remaining} -= $take;
+ $self->{length} += $take;
+
+ # Once the body parser is done (multipart is, at its close
+ # delimiter) the rest of the body is consumed but not kept.
+ unless ( $self->{state} eq 'done' ) {
+ $self->{buffer} .= $data;
+ $self->spin;
+ }
+
+ next;
+ }
+
+ # The CRLF that ends the data of the chunk just read.
+ if ( $self->{chunk_crlf} ) {
+ return if length( $self->{chunk_buffer} ) < 2;
- if ( $chunk_len == 0 ) {
- # Strip chunk len
- substr( $self->{chunk_buffer}, 0, $header_len, '' );
+ unless ( substr( $self->{chunk_buffer}, 0, 2, '' ) eq
"\x0D\x0A" ) {
+ Carp::croak('Chunk data not followed by CRLF');
+ }
+
+ $self->{chunk_crlf} = 0;
+ }
+
+ # After the last chunk: the trailer, header lines up to a blank
+ # line that ends the body. It is collected until then.
+ if ( defined $self->{chunk_trailer} ) {
+ my $line = $self->_chunk_line(
+ $max_trailer_size ? $max_trailer_size - length
$self->{chunk_trailer} : undef,
+ "Chunked body trailer exceeds $max_trailer_size bytes",
+ );
- # End of data, there may be trailing headers
- if ( my ($headers) = $self->{chunk_buffer} =~
m/(.*)\x0D\x0A/s ) {
- if ( my $message = HTTP::Message->parse( $headers ) ) {
+ # wait for the rest of the line
+ return unless defined $line;
+
+ if ( $line =~ /^\x0D?\x0A\z/ ) {
+ if ( my $message = HTTP::Message->parse(
$self->{chunk_trailer} ) ) {
$self->{trailing_headers} = $message->headers;
}
+
+ $self->{chunk_buffer} = '';
+ $self->{chunk_trailer} = undef;
+ $self->{chunk_eof} = 1;
+
+ return;
}
- $self->{chunk_buffer} = '';
- $self->{chunk_eof} = 1;
+ # HTTP::Headers takes time quadratic in the number of fields
+ # of one name, so the number of lines is limited as well.
+ if ( $max_trailer_lines && ++$self->{chunk_trailer_lines} >
$max_trailer_lines ) {
+ Carp::croak("Chunked body trailer exceeds
$max_trailer_lines lines");
+ }
- # Set content_length equal to the amount of data we read,
- # so the spin methods can finish up.
- $self->{content_length} = $self->{length};
+ $self->{chunk_trailer} .= $line;
+
+ # trailing_headers parses the trailer again when asked
+ delete $self->{trailing_headers};
+
+ next;
}
- else {
- # Make sure we have the whole chunk body plus its trailing CRLF
- # beyond the chunk-size line before we consume it. The old test
- # measured the whole buffer, size line included, so a read that
- # split a chunk left the parser taking it short and reparsing
the
- # leftover bytes as the next chunk-size line (never
completing).
- if ( length( $self->{chunk_buffer} ) >= $header_len +
$chunk_len + 2 ) {
- # Strip chunk len
- substr( $self->{chunk_buffer}, 0, $header_len, '' );
- # Pull chunk data out of chunk buffer into real buffer
- $self->{buffer} .= substr $self->{chunk_buffer}, 0,
$chunk_len, '';
+ # A chunk-size line.
+ my $line = $self->_chunk_line(
+ $max_chunk_line_size || undef,
+ "Chunk size line exceeds $max_chunk_line_size bytes",
+ );
- # Strip remaining CRLF
- $self->{chunk_buffer} =~ s/^\x0D\x0A//;
+ # wait for the rest of the line
+ return unless defined $line;
- $self->{length} += $chunk_len;
- }
- else {
- # Not enough data for this chunk, wait for more calls to
add()
- return;
- }
+ unless ( $line =~ $chunk_size_line ) {
+ Carp::croak('Invalid chunk size line');
+ }
+
+ # hex() of a size that does not fit an integer warns and returns a
+ # float, which no buffer length would ever reach.
+ ( my $digits = $1 ) =~ s/^0+//;
+ if ( length($digits) > 15 ) {
+ Carp::croak('Chunk size too large');
}
-
- unless ( $self->{state} eq 'done' ) {
- $self->spin;
+
+ my $chunk_len = hex($digits);
+
+ if ( $chunk_len == 0 ) {
+ # End of data: set content_length equal to the amount of data
+ # we read, so the spin methods can finish up.
+ $self->{content_length} = $self->{length};
+
+ unless ( $self->{state} eq 'done' ) {
+ $self->spin;
+ }
+
+ # The trailer follows, up to a blank line.
+ $self->{chunk_trailer} = '';
+
+ next;
}
+
+ $self->{chunk_remaining} = $chunk_len;
+ $self->{chunk_crlf} = 1;
}
-
+
return;
}
my $cl = $self->content_length;
if ( defined $_[0] ) {
- $self->{length} += length( $_[0] );
-
+ # A copy: the caller's buffer is left alone, and may be read-only.
+ my $data = $_[0];
+ $self->{length} += length($data);
+
# Don't allow buffer data to exceed content-length
if ( $self->{length} > $cl ) {
- $_[0] = substr $_[0], 0, $cl - $self->{length};
+ $data = substr $data, 0, $cl - $self->{length};
$self->{length} = $cl;
}
-
- $self->{buffer} .= $_[0];
+
+ $self->{buffer} .= $data;
}
unless ( $self->state eq 'done' ) {
@@ -277,6 +373,23 @@
return ( $self->length - $cl );
}
+# Takes the next line, LF included, off the chunk buffer, or returns nothing
+# while it is incomplete. Its length is checked before it is complete, so
+# that an endless line can not grow the buffer: longer than $room bytes, it
+# croaks with $error. An undefined $room is no limit.
+sub _chunk_line {
+ my ( $self, $room, $error ) = @_;
+
+ my $eol = index( $self->{chunk_buffer}, "\x0A" );
+ my $line_len = $eol >= 0 ? $eol + 1 : length $self->{chunk_buffer};
+
+ Carp::croak($error) if defined $room && $line_len > $room;
+
+ return if $eol < 0;
+
+ return substr( $self->{chunk_buffer}, 0, $line_len, '' );
+}
+
=item body
accessor for the body.
@@ -344,9 +457,8 @@
=item length
-Returns the total length of data we expect to read if known.
-In the case of a chunked request, returns the amount of data
-read so far.
+Returns the number of bytes of the body read so far, up to the content length.
+Of a chunked body only the chunk data counts.
=cut
@@ -357,12 +469,30 @@
=item trailing_headers
If a chunked request body had trailing headers, trailing_headers will
-return an HTTP::Headers object populated with those headers.
+return an HTTP::Headers object populated with those headers. The trailer is
+read up to the blank line that ends it, also when it arrives in later calls to
+add than the last, zero-size chunk; until then, trailing_headers returns the
+header lines that have arrived so far. The body itself is done at the last
+chunk.
=cut
sub trailing_headers {
- return shift->{trailing_headers};
+ my $self = shift;
+
+ # A trailer still being read gives the lines that have arrived of it, as
+ # some clients never send its blank line. It is parsed when asked for,
+ # not again for every line that arrives.
+ if ( !$self->{trailing_headers}
+ && defined $self->{chunk_trailer}
+ && length $self->{chunk_trailer} )
+ {
+ if ( my $message = HTTP::Message->parse( $self->{chunk_trailer} ) ) {
+ $self->{trailing_headers} = $message->headers;
+ }
+ }
+
+ return $self->{trailing_headers};
}
=item spin
@@ -523,7 +653,10 @@
=item $HTTP::Body::MultiPart::basename_regexp
The regexp used to work out the uploaded file's extension. It only matters
-when C<file_temp_suffix> is undefined.
+when C<file_temp_suffix> is undefined, and is matched against the last 255
+bytes of the uploaded file's basename, which is as long as a suffix can be.
+The default, C<qr/[^.]+(\.[^\\\/]+)$/>, is not run as a regexp: as long as the
+variable still holds it, a string search with the same result is used.
=item $HTTP::Body::MultiPart::file_temp_template
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/t/18chunked-size-line.t
new/HTTP-Body-1.25/t/18chunked-size-line.t
--- old/HTTP-Body-1.24/t/18chunked-size-line.t 2026-10-26 18:28:04.000000000
+0100
+++ new/HTTP-Body-1.25/t/18chunked-size-line.t 2026-11-05 18:47:11.000000000
+0100
@@ -3,7 +3,7 @@
use strict;
use warnings;
-use Test::More tests => 3;
+use Test::More tests => 5;
use HTTP::Body;
use Time::HiRes qw/ time /;
@@ -20,8 +20,17 @@
{
# A size line whose first LF has no CR before it never matches; the
# old pattern backtracked over it in time quadratic in its length.
+ # Without the line length limit the line reaches the pattern whole.
+ local $HTTP::Body::max_chunk_line_size = 0;
my $body = HTTP::Body->new( $ct, undef );
my $t = time;
- $body->add( ( '1' x 100_000 ) . "\n\r\n" );
+ eval { $body->add( ( '1' x 100_000 ) . "\n\r\n" ) };
cmp_ok( time - $t, '<', 5, 'unterminated chunk size line is rejected in
linear time' );
+ like( $@, qr/Invalid chunk size line/, '... and refused' );
+}
+
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ eval { $body->add( ( '1' x 100_000 ) . "\n\r\n" ) };
+ like( $@, qr/Chunk size line exceeds/, 'with the limit it is refused by
its length alone' );
}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/t/21filename-suffix-redos.t
new/HTTP-Body-1.25/t/21filename-suffix-redos.t
--- old/HTTP-Body-1.24/t/21filename-suffix-redos.t 1970-01-01
01:00:00.000000000 +0100
+++ new/HTTP-Body-1.25/t/21filename-suffix-redos.t 2026-11-05
18:47:11.000000000 +0100
@@ -0,0 +1,51 @@
+#!perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 5;
+
+use HTTP::Body;
+use File::Temp qw/ tempdir /;
+use Time::HiRes qw/ time /;
+
+my $ct = 'multipart/form-data; boundary=x';
+
+# A filename of many dots that ends in a backslash made the default
+# $basename_regexp backtrack in time quadratic in the filename's length:
+# about 7 seconds for a part whose header block fills the 32 KiB limit,
+# and that once per part, up to $max_parts times per body.
+my $hostile = ( 'a.' x 16000 ) . 'a\\';
+my $part = qq{--x\r\nContent-Disposition: form-data; name="f";
filename="$hostile"\r\n\r\nD\r\n};
+
+sub parse {
+ my $raw = shift;
+ my $body = HTTP::Body->new( $ct, length $raw );
+ $body->tmpdir( tempdir( CLEANUP => 1 ) );
+ $body->cleanup(1);
+ $body->add($raw);
+ return $body;
+}
+
+{
+ my $t = time;
+ my $body = parse( ( $part x 2 ) . "--x--\r\n" );
+ cmp_ok( time - $t, '<', 5, 'hostile filename is handled in linear time
with the fixed suffix' );
+ is( $body->state, 'done', 'body with hostile filenames is parsed' );
+}
+
+{
+ local $HTTP::Body::MultiPart::file_temp_suffix = undef;
+ my $t = time;
+ my $body = parse( ( $part x 2 ) . "--x--\r\n" );
+ cmp_ok( time - $t, '<', 5, 'hostile filename is handled in linear time
with the suffix taken from the filename' );
+ is( $body->state, 'done', 'body with hostile filenames is parsed in that
mode too' );
+}
+
+{
+ # The suffix is taken from the end of the filename, so a long name keeps
it.
+ local $HTTP::Body::MultiPart::file_temp_suffix = undef;
+ my $long = ( 'n' x 300 ) . '.txt';
+ my $body = parse( qq{--x\r\nContent-Disposition: form-data; name="f";
filename="$long"\r\n\r\nD\r\n--x--\r\n} );
+ like( $body->upload->{f}{tempname}, qr/\.txt$/, 'suffix of a filename
longer than 255 bytes is kept' );
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/t/22chunked-malformed.t
new/HTTP-Body-1.25/t/22chunked-malformed.t
--- old/HTTP-Body-1.24/t/22chunked-malformed.t 1970-01-01 01:00:00.000000000
+0100
+++ new/HTTP-Body-1.25/t/22chunked-malformed.t 2026-11-05 18:47:11.000000000
+0100
@@ -0,0 +1,76 @@
+#!perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 12;
+
+use HTTP::Body;
+
+my $ct = 'application/octet-stream';
+
+# A line that is not a chunk-size line used to end the parse silently, with
+# every later add() growing the chunk buffer forever.
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ eval { $body->add("zzz\r\n") };
+ like( $@, qr/Invalid chunk size line/, 'a line that is no chunk-size line
is refused' );
+}
+
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ eval { $body->add( '1' x 2000 ) };
+ like( $@, qr/Chunk size line exceeds/, 'an unterminated chunk-size line
over the limit is refused' );
+}
+
+{
+ local $HTTP::Body::max_chunk_line_size = 0;
+ my $body = HTTP::Body->new( $ct, undef );
+ eval { $body->add( '1' x 2000 ) };
+ is( $@, '', 'the chunk-size line limit can be disabled' );
+}
+
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ my @warnings;
+ local $SIG{__WARN__} = sub { push @warnings, @_ };
+ eval { $body->add( ( 'F' x 40 ) . "\r\n" ) };
+ like( $@, qr/Chunk size too large/, 'a chunk size beyond the integer range
is refused' );
+ is( scalar @warnings, 0, '... without an overflow warning' );
+}
+
+# A chunk used to be held back until it had arrived completely, so a large
+# chunk needed as much memory as it was long.
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ $body->add( sprintf "%x\r\n", 1_000_000 );
+ $body->add( 'x' x 8192 ) for 1 .. 10;
+ is( $body->length, 81920, 'chunk data reaches the body before the chunk is
complete' );
+}
+
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ eval { $body->add("5\r\nhelloXX") };
+ like( $@, qr/not followed by CRLF/, 'chunk data not followed by CRLF is
refused' );
+}
+
+# Nothing after the last chunk is kept.
+{
+ my $body = HTTP::Body->new( 'application/x-www-form-urlencoded', undef );
+ $body->add("3\r\na=b\r\n0\r\n\r\n");
+ $body->add( 'z' x 100_000 );
+ is( $body->state, 'done', 'body stays done after the last chunk' );
+ is( length $body->{chunk_buffer}, 0, 'data after the last chunk is
dropped' );
+ is_deeply( $body->param, { a => 'b' }, 'parameters are unchanged by it' );
+}
+
+# A multipart body is done at its close delimiter; later chunks are not kept.
+{
+ my $mp = qq{--x\r\nContent-Disposition: form-data;
name="a"\r\n\r\nv\r\n--x--\r\n};
+ my $body = HTTP::Body->new( 'multipart/form-data; boundary=x', undef );
+ $body->add( sprintf "%x\r\n%s\r\n", length $mp, $mp );
+ my $chunk = 'y' x 10_000;
+ $body->add( sprintf "%x\r\n%s\r\n", length $chunk, $chunk ) for 1 .. 10;
+ is( length $body->{buffer}, 0, 'chunk data after the body parser is done
is dropped' );
+ is( $body->param->{a}, 'v', 'the parsed parameter is kept' );
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/t/23xforms-attachment.t
new/HTTP-Body-1.25/t/23xforms-attachment.t
--- old/HTTP-Body-1.24/t/23xforms-attachment.t 1970-01-01 01:00:00.000000000
+0100
+++ new/HTTP-Body-1.25/t/23xforms-attachment.t 2026-11-05 18:47:11.000000000
+0100
@@ -0,0 +1,35 @@
+#!perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 5;
+
+use HTTP::Body;
+use File::Temp qw/ tempdir /;
+
+# A multipart/related part other than the start part is an upload named after
+# its Content-ID. XFormsMultipart used to set its name before the MultiPart
+# handler ran, which skipped both the NUL strip of the filename (RT#123615)
+# and the creation of the temporary file, so the attachment was dropped.
+my $raw = qq{--x\r\nContent-ID: <model>\r\nContent-Type:
application/xml\r\n\r\n<m/>\r\n}
+ . qq{--x\r\nContent-ID: <att\0ach>\r\nContent-Type:
text/plain\r\n\r\nATTACHMENT DATA\r\n--x--\r\n};
+
+my $body = HTTP::Body->new( 'multipart/related; boundary=x; start="<model>"',
length $raw );
+$body->tmpdir( tempdir( CLEANUP => 1 ) );
+$body->cleanup(1);
+$body->add($raw);
+
+is( $body->param->{'XForms:Model'}, '<m/>', 'the start part is the model' );
+
+my ($upload) = values %{ $body->upload };
+is( $upload->{filename}, 'attach', 'NUL is stripped from the attachment
filename' );
+is( $upload->{name}, 'attach', '... and from its name' );
+ok( $upload->{tempname} && -e $upload->{tempname}, 'the attachment is written
to a temporary file' );
+
+my $data = '';
+if ( $upload->{tempname} && open my $fh, '<', $upload->{tempname} ) {
+ local $/;
+ $data = <$fh>;
+}
+is( $data, 'ATTACHMENT DATA', 'the temporary file holds the attachment data' );
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/t/24part-header-lines.t
new/HTTP-Body-1.25/t/24part-header-lines.t
--- old/HTTP-Body-1.24/t/24part-header-lines.t 1970-01-01 01:00:00.000000000
+0100
+++ new/HTTP-Body-1.25/t/24part-header-lines.t 2026-11-05 18:47:11.000000000
+0100
@@ -0,0 +1,34 @@
+#!perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 4;
+
+use HTTP::Body;
+
+my $ct = 'multipart/form-data; boundary=x';
+
+{
+ # A continuation line with no header line before it died in parse_header
+ # with "Modification of non-creatable array value attempted".
+ my $raw = qq{--x\r\n junk\r\nContent-Disposition: form-data;
name="a"\r\n\r\nv\r\n--x--\r\n};
+ my $body = HTTP::Body->new( $ct, length $raw );
+ my $ok = eval { $body->add($raw); 1 };
+ ok( $ok, 'a part header block that starts with whitespace does not die' )
or diag $@;
+ is( $body->param->{a}, 'v', '... and the part is parsed' );
+}
+
+{
+ # A line that is no header field was stored under whatever $1 was left
+ # over from the last successful match.
+ my $raw = qq{--x\r\nContent-Disposition: form-data; name="a"\r\nthis is
not a header\r\n\r\nv\r\n--x--\r\n};
+ my $body = HTTP::Body->new( $ct, length $raw );
+ $body->add($raw);
+ is( $body->param->{a}, 'v', 'a part with a line that is no header field is
parsed' );
+ is_deeply(
+ [ sort keys %{ $body->part_data->{a}{headers} } ],
+ ['Content-Disposition'],
+ 'the line is not stored under a made-up field name'
+ );
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/t/25add-keeps-argument.t
new/HTTP-Body-1.25/t/25add-keeps-argument.t
--- old/HTTP-Body-1.24/t/25add-keeps-argument.t 1970-01-01 01:00:00.000000000
+0100
+++ new/HTTP-Body-1.25/t/25add-keeps-argument.t 2026-11-05 18:47:11.000000000
+0100
@@ -0,0 +1,24 @@
+#!perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 3;
+
+use HTTP::Body;
+
+my $ct = 'application/x-www-form-urlencoded';
+
+{
+ my $body = HTTP::Body->new( $ct, 3 );
+ my $buf = 'a=b&c=d';
+ $body->add($buf);
+ is( $buf, 'a=b&c=d', "add leaves the caller's buffer alone when it exceeds
the content length" );
+ is_deeply( $body->param, { a => 'b' }, '... and keeps only the content
length' );
+}
+
+{
+ my $body = HTTP::Body->new( $ct, 3 );
+ my $ok = eval { $body->add('a=b&c=d'); 1 };
+ ok( $ok, 'add accepts a read-only value that exceeds the content length' )
or diag $@;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/t/26filename-suffix-string-search.t
new/HTTP-Body-1.25/t/26filename-suffix-string-search.t
--- old/HTTP-Body-1.24/t/26filename-suffix-string-search.t 1970-01-01
01:00:00.000000000 +0100
+++ new/HTTP-Body-1.25/t/26filename-suffix-string-search.t 2026-11-05
18:47:11.000000000 +0100
@@ -0,0 +1,85 @@
+#!perl
+
+use strict;
+use warnings;
+
+use Test::More;
+
+use HTTP::Body;
+use File::Temp qw/ tempdir /;
+use Time::HiRes qw/ time /;
+
+# While $basename_regexp holds its default, the suffix is found by a string
+# search instead of by running the regexp. The two must give the same
+# result for every basename.
+
+my $default = qr/[^.]+(\.[^\\\/]+)$/;
+
+sub by_regexp { $_[0] =~ $default ? $1 : q{} }
+
+sub show { my $s = shift; $s =~ s/([^\x21-\x7e])/sprintf '\\x%02x', ord $1/ge;
"'$s'" }
+
+can_ok( 'HTTP::Body::MultiPart', '_default_suffix' );
+my $suffix = \&HTTP::Body::MultiPart::_default_suffix;
+
+is( "$HTTP::Body::MultiPart::basename_regexp", "$default", 'default
$basename_regexp is unchanged' );
+
+for my $name (
+ '', '.', '..', 'x.', '.bashrc', 'noext', 'a..b', '..a.b', 'a.b.',
+ 'foo.tar.gz', 'photo.jpeg', 'dir/x.png', 'a.b/c', 'a.b\\',
+ 'C:\\dir\\x.txt', 'C:\\a.d\\bar', 'dir\\.hidden', "f.txt\n", "f.\n",
+ "\xc3\xa4rger.txt", ( 'a.' x 127 ) . "a\\",
+) {
+ is( $suffix->($name), by_regexp($name), 'suffix of ' . show($name) );
+}
+
+{
+ srand 20261027;
+ my @alpha = ( '.', '.', '.', 'a', 'b', '\\', '/', "\n", ' ', "\xff" );
+ my ( $bad, $first ) = ( 0, undef );
+ for ( 1 .. 100_000 ) {
+ my $name = join '', map { $alpha[ rand @alpha ] } 1 .. int rand 12;
+ next if $suffix->($name) eq by_regexp($name);
+ $first = $name unless defined $first;
+ $bad++;
+ }
+ is( $bad, 0, 'string search matches the regexp on 100000 random basenames'
)
+ or diag( 'first mismatch: ' . show($first) );
+}
+
+my $ct = 'multipart/form-data; boundary=x';
+
+sub upload_tempname {
+ my $filename = shift;
+ my $raw = qq{--x\r\nContent-Disposition: form-data; name="f";
filename="$filename"\r\n\r\nD\r\n--x--\r\n};
+ my $body = HTTP::Body->new( $ct, length $raw );
+ $body->tmpdir( tempdir( CLEANUP => 1 ) );
+ $body->cleanup(1);
+ $body->add($raw);
+ return $body->upload->{f}{tempname};
+}
+
+{
+ local $HTTP::Body::MultiPart::file_temp_suffix = undef;
+ like( upload_tempname('C:\\photos\\holiday.tar.gz'), qr/\.tar\.gz$/,
'default suffix of an upload comes from the string search' );
+}
+
+{
+ # A regexp of its own is still run, on the last 255 bytes of the basename.
+ local $HTTP::Body::MultiPart::file_temp_suffix = undef;
+ local $HTTP::Body::MultiPart::basename_regexp = qr/(\.[^.]+)$/;
+ like( upload_tempname('archive.tar.gz'), qr/\.gz$/, 'a custom
$basename_regexp is still used' );
+ unlike( upload_tempname('archive.tar.gz'), qr/\.tar\.gz$/, 'a custom
$basename_regexp replaces the default rule' );
+}
+
+{
+ # A copy of the default pattern is a regexp of its own and is run, so the
+ # 255-byte limit is what keeps a hostile filename cheap.
+ local $HTTP::Body::MultiPart::file_temp_suffix = undef;
+ local $HTTP::Body::MultiPart::basename_regexp = qr/[^.]+(\.[^\\\/]+)$/;
+ my $t = time;
+ upload_tempname( ( 'a.' x 16000 ) . 'a\\' ) for 1 .. 2;
+ cmp_ok( time - $t, '<', 5, 'a hostile filename stays cheap with a custom
$basename_regexp' );
+}
+
+done_testing;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/HTTP-Body-1.24/t/27chunked-trailer.t
new/HTTP-Body-1.25/t/27chunked-trailer.t
--- old/HTTP-Body-1.24/t/27chunked-trailer.t 1970-01-01 01:00:00.000000000
+0100
+++ new/HTTP-Body-1.25/t/27chunked-trailer.t 2026-11-05 18:47:11.000000000
+0100
@@ -0,0 +1,103 @@
+#!perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 20;
+
+use HTTP::Body;
+
+my $ct = 'application/x-www-form-urlencoded';
+
+# The trailing headers, or none while there is no trailer.
+sub trailer { my $h = shift->trailing_headers; return $h ? $h :
HTTP::Headers->new }
+
+# The trailer of a chunked body used to be read only from the call to add()
+# that carried the last chunk; a trailer that arrived in a later call was lost.
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ $body->add("3\r\na=b\r\n0\r\n");
+ is( $body->state, 'done', 'the body is done at the last chunk' );
+ is_deeply( $body->param, { a => 'b' }, '... with its parameters' );
+ is( $body->trailing_headers, undef, 'no trailer before it has arrived' );
+
+ $body->add("X-Foo: bar\r\n");
+ isa_ok( $body->trailing_headers, 'HTTP::Headers', 'trailer from a later
add' );
+ is( trailer($body)->header('X-Foo'), 'bar', '... with its field' );
+
+ $body->add("X-Baz: qux\r\n\r\n");
+ is( trailer($body)->header('X-Baz'), 'qux', '... and the fields that
follow it' );
+}
+
+# Some clients never send the blank line that ends the trailer; what has
+# arrived of it is read all the same, as it was before.
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ $body->add("3\r\na=b\r\n0\r\nContent-MD5: abc\r\nX-Half");
+ is( trailer($body)->header('Content-MD5'), 'abc', 'a trailer without its
blank line' );
+ is( trailer($body)->header('X-Half'), undef, '... but not a line still
incomplete' );
+}
+
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ $body->add($_) for split //, "3\r\na=b\r\n0\r\nX-Foo: bar\r\nX-Baz:
qux\r\n\r\n";
+ is( trailer($body)->header('X-Foo'), 'bar', 'trailer fed byte by byte' );
+ is( trailer($body)->header('X-Baz'), 'qux', '... has all its fields' );
+}
+
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ $body->add("3\r\na=b\r\n0\r\n\r\n");
+ isa_ok( $body->trailing_headers, 'HTTP::Headers', 'an empty trailer' );
+ is( scalar trailer($body)->header_field_names, 0, '... has no fields' );
+}
+
+# The blank line ends the body: nothing after it is read as a trailer field.
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ $body->add("3\r\na=b\r\n0\r\nX-Foo: bar\r\n\r\nX-Evil: 1\r\n");
+ $body->add("X-Evil2: 1\r\n\r\n");
+ is_deeply( [ trailer($body)->header_field_names ], ['X-Foo'], 'nothing
after the trailer is read' );
+ is( length $body->{chunk_buffer}, 0, '... or kept' );
+}
+
+# The trailer is collected until its blank line, so it is limited in size.
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ $body->add("3\r\na=b\r\n0\r\n");
+ eval { $body->add( 'X-Foo: ' . ( 'b' x 991 ) . "\r\n" ) for 1 .. 40 };
+ like( $@, qr/trailer exceeds 32768 bytes/, 'a trailer over the size limit
is refused' );
+}
+
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ $body->add("3\r\na=b\r\n0\r\n");
+ eval { $body->add( 'X' x 1000 ) for 1 .. 40 };
+ like( $@, qr/trailer exceeds 32768 bytes/, 'an unterminated trailer line
over the limit is refused' );
+}
+
+# HTTP::Headers takes time quadratic in the number of fields of one name,
+# which made a trailer of 16 KiB of "a:" lines cost seconds; and 1.24 parsed
+# whatever followed the last chunk in the same call to add().
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ $body->add("3\r\na=b\r\n0\r\n");
+ eval { $body->add("a:\r\n") for 1 .. 101 };
+ like( $@, qr/trailer exceeds 100 lines/, 'a trailer over the line limit is
refused' );
+}
+
+{
+ my $body = HTTP::Body->new( $ct, undef );
+ $body->add( "3\r\na=b\r\n0\r\n" . ( "a:\r\n" x 100 ) . "\r\n" );
+ is( scalar( () = trailer($body)->header('a') ), 100, 'a trailer at the
line limit is read' );
+}
+
+{
+ local $HTTP::Body::max_trailer_size = 0;
+ local $HTTP::Body::max_trailer_lines = 0;
+ my $body = HTTP::Body->new( $ct, undef );
+ $body->add("3\r\na=b\r\n0\r\n");
+ eval { $body->add( 'X-Foo: ' . ( 'b' x 991 ) . "\r\n" ) for 1 .. 200;
$body->add("\r\n") };
+ is( $@, '', 'the trailer limits can be disabled' );
+ is( scalar( () = trailer($body)->header('X-Foo') ), 200, '... and the
whole trailer is read' );
+}
++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.vvjcaQ/_old 2026-11-06 20:07:35.433416637 +0100
+++ /var/tmp/diff_new_pack.vvjcaQ/_new 2026-11-06 20:07:35.440416930 +0100
@@ -1,6 +1,6 @@
-mtime: 1793220534
-commit: 25896feeebed535d080544261f74f7f90a945129e2ec54260afdb6b45b9a870e
+mtime: 1793976378
+commit: ea4c45316973ad0657521ac1fb018f72446917a2a2378a8eebda0a961ec0fe7a
url: https://src.opensuse.org/perl/perl-HTTP-Body
-revision: 25896feeebed535d080544261f74f7f90a945129e2ec54260afdb6b45b9a870e
+revision: ea4c45316973ad0657521ac1fb018f72446917a2a2378a8eebda0a961ec0fe7a
projectscmsync: https://src.opensuse.org/perl/_ObsPrj
++++++ build.specials.obscpio ++++++
++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore 1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore 2026-11-06 15:46:18.000000000 +0100
@@ -0,0 +1 @@
+.osc