Version in base suite: 0.18-1 Base version: libspreadsheet-xlsx-perl_0.18-1 Target version: libspreadsheet-xlsx-perl_0.20-0+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/libs/libspreadsheet-xlsx-perl/libspreadsheet-xlsx-perl_0.18-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/libs/libspreadsheet-xlsx-perl/libspreadsheet-xlsx-perl_0.20-0+deb13u1.dsc Changes | 19 +++++ MANIFEST | 6 + META.json | 2 META.yml | 2 README | 80 +++++++++++++++++++++--- cpanfile | 18 +++++ debian/changelog | 24 +++++++ lib/Spreadsheet/XLSX.pm | 120 ++++++++++++++++++++++++++++-------- lib/Spreadsheet/XLSX/Fmt2007.pm | 2 lib/Spreadsheet/XLSX/Utility2007.pm | 27 +++++++- t/cell_references.t | 42 ++++++++++++ t/date_limits.t | 41 ++++++++++++ t/format_conditions.t | 48 ++++++++++++++ t/number_format_ids.t | 45 +++++++++++++ t/zip_limits.t | 118 +++++++++++++++++++++++++++++++++++ 15 files changed, 555 insertions(+), 39 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpmdc9imji/libspreadsheet-xlsx-perl_0.18-1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpmdc9imji/libspreadsheet-xlsx-perl_0.20-0+deb13u1.dsc: no acceptable signature found diff -Nru libspreadsheet-xlsx-perl-0.18/Changes libspreadsheet-xlsx-perl-0.20/Changes --- libspreadsheet-xlsx-perl-0.18/Changes 2024-03-09 20:08:39.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/Changes 2026-10-29 21:18:37.000000000 +0000 @@ -1,5 +1,24 @@ Revision history for Perl extension Spreadsheet::XLSX. +0.20 2026-10-29 + + - Security: limit ZIP entry counts and uncompressed sizes; read members + incrementally and reject inconsistent size metadata. + - Security: validate number format IDs and escape regex lookups to prevent + regex injection from crafted styles. + - Security: reject out-of-range date values to prevent excessive CPU usage. + - Security: validate cell references against XLSX worksheet limits before + indexing cell arrays, preventing excessive allocation from invalid rows. + - Add cpanfile for easy dependency installation. + - Improve Readme. + +0.19 2026-10-29 + + - Security: replace string eval of number-format conditions with strict + numeric comparisons; invalid conditions use the fallback format section. + Prevents arbitrary Perl code execution when parsing crafted XLSX files. + - Add regression tests for numeric conditions and malicious XLSX formats. + 0.18 - fix RT #117166: unformatted date values can no longer be retrieved, thx to lherschi diff -Nru libspreadsheet-xlsx-perl-0.18/MANIFEST libspreadsheet-xlsx-perl-0.20/MANIFEST --- libspreadsheet-xlsx-perl-0.18/MANIFEST 2023-12-31 13:23:06.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/MANIFEST 2026-10-29 21:13:04.000000000 +0000 @@ -1,4 +1,5 @@ Changes +cpanfile lib/Spreadsheet/XLSX.pm lib/Spreadsheet/XLSX/Fmt2007.pm lib/Spreadsheet/XLSX/Utility2007.pm @@ -16,6 +17,11 @@ t/empty_v_tag.t t/empty_v_tag.xlsx t/formats.t +t/format_conditions.t +t/cell_references.t +t/date_limits.t +t/number_format_ids.t +t/zip_limits.t t/formats.xlsx t/kwalitee.t t/missing_styles.t diff -Nru libspreadsheet-xlsx-perl-0.18/META.json libspreadsheet-xlsx-perl-0.20/META.json --- libspreadsheet-xlsx-perl-0.18/META.json 2024-03-09 20:13:23.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/META.json 2026-10-29 21:20:02.000000000 +0000 @@ -51,6 +51,6 @@ "url" : "https://github.com/asb-capfan/Spreadsheet-XLSX" } }, - "version" : "0.18", + "version" : "0.20", "x_serialization_backend" : "JSON::PP version 4.04" } diff -Nru libspreadsheet-xlsx-perl-0.18/META.yml libspreadsheet-xlsx-perl-0.20/META.yml --- libspreadsheet-xlsx-perl-0.18/META.yml 2024-03-09 20:13:21.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/META.yml 2026-10-29 21:20:00.000000000 +0000 @@ -26,5 +26,5 @@ Spreadsheet::ParseExcel: '0' resources: repository: https://github.com/asb-capfan/Spreadsheet-XLSX -version: '0.18' +version: '0.20' x_serialization_backend: 'CPAN::Meta::YAML version 0.018' diff -Nru libspreadsheet-xlsx-perl-0.18/README libspreadsheet-xlsx-perl-0.20/README --- libspreadsheet-xlsx-perl-0.18/README 2023-12-31 13:22:41.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/README 2026-10-29 21:17:37.000000000 +0000 @@ -14,22 +14,82 @@ DEPENDENCIES -This module requires these other modules and libraries: +Dependencies are listed in cpanfile. From the repository root, install the +runtime and test dependencies with cpanminus: - Archive::Zip - Spreadsheet::ParseExcel + cpanm --installdeps . + +For development and release testing, include the develop dependencies: + + cpanm --installdeps --with-develop . + +If cpanm is not installed, install it first: + + cpan App::cpanminus BUILD AND RELEASE INSTRUCTIONS -Note: Meta files (META.json and META.yml) have to be generated using ExtUtils::MakeMaker to satisfy the MANIFEST file. +Run the following commands from the root of the checked-out repository. +On Windows with Strawberry Perl, use gmake instead of make throughout. +Building the .tar.gz archive also requires tar and gzip on PATH. + +1. Prepare the release. + + Update the version in lib/Spreadsheet/XLSX.pm, + lib/Spreadsheet/XLSX/Fmt2007.pm and lib/Spreadsheet/XLSX/Utility2007.pm. + Update Changes with the release version and date, and add any new + distribution files to MANIFEST. + +2. Install dependencies and generate the Makefile. + + Install all dependencies from cpanfile, including release test modules: + + cpanm --installdeps --with-develop . + perl Makefile.PL + + Test::Kwalitee and Test::Warnings are required to run the release-only + tests; those tests are skipped if the modules are missing. + +3. Run the regular tests from the checkout. + + prove -lr t + +4. Build a distribution directory and run all tests there. + + In a POSIX shell: + + RELEASE_TESTING=1 make disttest + + In PowerShell with Strawberry Perl: + + $env:RELEASE_TESTING = '1' + make disttest + Remove-Item Env:RELEASE_TESTING + + Check that all tests pass, including the release-only tests. + disttest creates the distribution directory and generates META.json and + META.yml before testing. These files need not exist in the checkout; + running the release-only tests directly there can therefore report a + missing META.yml. An initial Makefile.PL warning about missing META files + is expected in a fresh checkout. + +5. Create the release archive after the tests pass. + + make dist + + This creates Spreadsheet-XLSX-.tar.gz, including META.json and + META.yml. Building the archive does not upload or publish the release. + +6. Remove temporary build files. + + make realclean -Use the commands: + realclean removes build files, blib, MYMETA.json, MYMETA.yml, the Makefile + and the generated distribution directory. The release .tar.gz archive + remains available for upload. make clean performs a partial cleanup + and leaves Makefile.old behind. - perl Makefile.PL - make distdir - make disttest - make dist - + To build again after realclean, start with perl Makefile.PL. COPYRIGHT AND LICENCE diff -Nru libspreadsheet-xlsx-perl-0.18/cpanfile libspreadsheet-xlsx-perl-0.20/cpanfile --- libspreadsheet-xlsx-perl-0.18/cpanfile 1970-01-01 00:00:00.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/cpanfile 2026-10-29 21:13:04.000000000 +0000 @@ -0,0 +1,18 @@ +requires 'perl', '5.006000'; +requires 'Archive::Zip', '1.18'; +requires 'Spreadsheet::ParseExcel'; + +on 'configure' => sub { + requires 'ExtUtils::MakeMaker'; +}; + +on 'test' => sub { + requires 'Test::More'; + requires 'Test::Warnings'; + requires 'Test::NoWarnings'; + requires 'File::Temp'; +}; + +on 'develop' => sub { + requires 'Test::Kwalitee'; +}; diff -Nru libspreadsheet-xlsx-perl-0.18/debian/changelog libspreadsheet-xlsx-perl-0.20/debian/changelog --- libspreadsheet-xlsx-perl-0.18/debian/changelog 2024-03-16 22:58:20.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/debian/changelog 2026-11-09 16:28:55.000000000 +0000 @@ -1,3 +1,27 @@ +libspreadsheet-xlsx-perl (0.20-0+deb13u1) trixie-security; urgency=high + + * Non-maintainer upload by the Security Team. + + [ gregor herrmann ] + * Import upstream version 0.20. + + From 0.19: + - Security: replace string eval of number-format conditions with strict + numeric comparisons; invalid conditions use the fallback format section. + Prevents arbitrary Perl code execution when parsing crafted XLSX files. + A.k.a CVE-2026-115943 (closes: #1152516) + + From 0.20: + - Security: limit ZIP entry counts and uncompressed sizes; read members + incrementally and reject inconsistent size metadata. + - Security: validate number format IDs and escape regex lookups to prevent + regex injection from crafted styles. + - Security: reject out-of-range date values to prevent excessive CPU usage. + - Security: validate cell references against XLSX worksheet limits before + indexing cell arrays, preventing excessive allocation from invalid rows. + + -- Salvatore Bonaccorso Mon, 09 Nov 2026 17:28:55 +0100 + libspreadsheet-xlsx-perl (0.18-1) unstable; urgency=medium [ Debian Janitor ] diff -Nru libspreadsheet-xlsx-perl-0.18/lib/Spreadsheet/XLSX/Fmt2007.pm libspreadsheet-xlsx-perl-0.20/lib/Spreadsheet/XLSX/Fmt2007.pm --- libspreadsheet-xlsx-perl-0.18/lib/Spreadsheet/XLSX/Fmt2007.pm 2024-03-09 20:12:16.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/lib/Spreadsheet/XLSX/Fmt2007.pm 2026-10-29 21:09:41.000000000 +0000 @@ -3,7 +3,7 @@ use warnings; use Spreadsheet::XLSX::Utility2007 qw(ExcelFmt); -our $VERSION = '0.18'; # +our $VERSION = '0.20'; # my %hFmtDefault = ( 0x00 => '@', diff -Nru libspreadsheet-xlsx-perl-0.18/lib/Spreadsheet/XLSX/Utility2007.pm libspreadsheet-xlsx-perl-0.20/lib/Spreadsheet/XLSX/Utility2007.pm --- libspreadsheet-xlsx-perl-0.18/lib/Spreadsheet/XLSX/Utility2007.pm 2024-03-09 20:12:22.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/lib/Spreadsheet/XLSX/Utility2007.pm 2026-10-29 21:09:35.000000000 +0000 @@ -13,13 +13,28 @@ use vars qw(@ISA @EXPORT_OK); @ISA = qw(Exporter); @EXPORT_OK = qw(ExcelFmt LocaltimeExcel ExcelLocaltime col2int int2col sheetRef xls2csv); -our $VERSION = '0.18'; +our $VERSION = '0.20'; my $sNUMEXP = '(^[+-]?\d+(\.\d+)?$)|(^[+-]?\d+\.?(\d*)[eE][+-](\d+))$'; #------------------------------------------------------------------------------ # ExcelFmt (for Spreadsheet::XLSX::Utility2007) #------------------------------------------------------------------------------ +# Treat format conditions as data. Invalid conditions select the fallback section. +sub _matches_condition { + my ($value, $condition) = @_; + my $number = qr/[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?/; + return 0 unless defined($value) && $value =~ /\A\s*$number\s*\z/; + return 0 unless $condition =~ /\A\s*(>=|<=|<>|==|>|<|=)\s*($number)\s*\z/; + my ($operator, $operand) = ($1, $2); + return $value > $operand if $operator eq '>'; + return $value >= $operand if $operator eq '>='; + return $value < $operand if $operator eq '<'; + return $value <= $operand if $operator eq '<='; + return $value != $operand if $operator eq '<>'; + return $value == $operand; +} + sub ExcelFmt { my ($sFmt, $iData, $i1904, $sType) = @_; my $sCond; @@ -69,7 +84,7 @@ #Get FmtString if (scalar(@sFmtWk) > 1) { if ($sCond) { - $sFmtObj = $sFmtWk[((eval(qq/"$iData" $sCond/)) ? 0 : 1)]; + $sFmtObj = $sFmtWk[(_matches_condition($iData, $sCond) ? 0 : 1)]; } else { my $iWk = ($iData =~ /$sNUMEXP/) ? $iData : 0; @@ -697,6 +712,14 @@ #------------------------------------------------------------------------------ sub ExcelLocaltime { my ($dObj, $flg1904) = @_; + # Excel dates end on 9999-12-31; allow the fractional time on that day. + # Check before int() and the year loop, including numbers that overflow + # to infinity when converted from scientific notation. + my $limit = $flg1904 ? 2957004 : 2958466; + die "Invalid or out-of-range XLSX date value\n" + unless defined($dObj) + && $dObj =~ /\A[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?\z/ + && $dObj >= 0 && $dObj < $limit; my ($iSec, $iMin, $iHour, $iDay, $iMon, $iYear, $iwDay, $iMSec); my ($iDt, $iTime, $iYDays); diff -Nru libspreadsheet-xlsx-perl-0.18/lib/Spreadsheet/XLSX.pm libspreadsheet-xlsx-perl-0.20/lib/Spreadsheet/XLSX.pm --- libspreadsheet-xlsx-perl-0.18/lib/Spreadsheet/XLSX.pm 2024-03-09 20:12:01.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/lib/Spreadsheet/XLSX.pm 2026-10-29 21:09:26.000000000 +0000 @@ -6,7 +6,11 @@ use base 'Spreadsheet::ParseExcel::Workbook'; -our $VERSION = '0.18'; +our $VERSION = '0.20'; + +our $MAX_MEMBER_SIZE = 64 * 1024 * 1024; +our $MAX_TOTAL_SIZE = 256 * 1024 * 1024; +our $MAX_MEMBERS = 10000; use Archive::Zip; use Spreadsheet::ParseExcel; @@ -42,11 +46,12 @@ $self->{SheetCount} = 0; $self->{FmtClass} = Spreadsheet::XLSX::Fmt2007->new; $self->{Flg1904} = 0; - if ($member_workbook->contents =~ /date1904="1"/) { + my $workbook_xml = __member_contents($member_workbook); + if ($workbook_xml =~ /date1904="1"/) { $self->{Flg1904} = 1; } - foreach ($member_workbook->contents =~ /\<(.*?)\/?\>/g) { + foreach ($workbook_xml =~ /\<(.*?)\/?\>/g) { /^(\w+)\s+/; @@ -95,10 +100,12 @@ my $s = 0; my $s2 = 0; my $sty = 0; - foreach ($member_sheet->contents =~ /(\<.*?\/?\>|.*?(?=\<))/g) { - if (/^\|.*?(?=\<))/g) { + if (/^)/) { - ($row, $col) = __decode_cell_name($1, $2, $3); + my ($reference) = /\sr="([^"]*)"/; + ($row, $col) = __decode_cell_name($reference); $s = m/t=\"s\"/ ? 1 : 0; $s2 = m/t=\"str\"/ ? 1 : 0; @@ -163,22 +170,22 @@ return $self; } -# Convert cell name in the format AA1 to a row and column number. +# Validate before numeric conversion or using a reference as an array index. +# XLSX worksheets have at most 1,048,576 rows and 16,384 columns (XFD). sub __decode_cell_name { - my ($letter1, $letter2, $digits) = @_; - - my $col = ord($letter1) - 65; - - if ($letter2) { - $col++; - $col *= 26; - $col += (ord($letter2) - 65); - } + my ($reference) = @_; + die "Invalid XLSX cell reference\n" + unless defined($reference) + && $reference =~ /\A([A-Z]{1,3})([1-9][0-9]{0,6})\z/; + my ($letters, $digits) = ($1, $2); + die "XLSX cell row exceeds worksheet limit\n" if $digits > 1048576; + + my $col = 0; + $col = $col * 26 + ord($_) - ord('A') + 1 for split //, $letters; + die "XLSX cell column exceeds worksheet limit\n" if $col > 16384; - my $row = $digits - 1; - - return ($row, $col); + return ($digits - 1, $col - 1); } @@ -191,7 +198,7 @@ if ($member_shared_strings) { - my $mstr = $member_shared_strings->contents; + my $mstr = __member_contents($member_shared_strings); $mstr =~ s//<\/t>/gsm; # this handles an empty t tag in the xml foreach my $si ($mstr =~ /(.*?)<\/si/gsm) { my $str; @@ -217,15 +224,22 @@ if ($member_styles) { my $formatter = Spreadsheet::XLSX::Fmt2007->new(); + my $styles_xml = __member_contents($member_styles); + + # IDs from both format declarations and style references are data, + # never regular-expression syntax. + foreach my $id ($styles_xml =~ /\bnumFmtId="([^"]*)"/g) { + die "Invalid XLSX number format ID\n" unless $id =~ /\A[0-9]+\z/; + } - foreach my $t ($member_styles->contents =~ /xf\ numFmtId="([^"]*)"(?!.*\/cellStyleXfs)/gsm) { #" + foreach my $t ($styles_xml =~ /xf\ numFmtId="([^"]*)"(?!.*\/cellStyleXfs)/gsm) { #" push @styles, $t; } my $default = $1 || ''; foreach my $t1 (@styles) { - $member_styles->contents =~ /numFmtId="$t1" formatCode="([^"]*)/; + $styles_xml =~ /numFmtId="\Q$t1\E" formatCode="([^"]*)/; my $formatCode = $1 || ''; if ($formatCode eq $default || not($formatCode)) { if ($t1 == 9 || $t1 == 10) { @@ -253,7 +267,8 @@ my %rels = (); - foreach ($member_rels->contents =~ /\/g) { + my $rels_xml = __member_contents($member_rels); + foreach ($rels_xml =~ /\/g) { my ($id, $target); ($id) = /Id="(.*?)"/; @@ -279,9 +294,50 @@ $zip->read($filename) == Archive::Zip::AZ_OK or die("Cannot open $filename as Zip archive"); } + my @members = $zip->members(); + die "XLSX ZIP entry count exceeds limit\n" if @members > $MAX_MEMBERS; + my $total = 0; + foreach my $member (@members) { + my $size = $member->uncompressedSize(); + die "XLSX ZIP member size exceeds limit\n" if $size > $MAX_MEMBER_SIZE; + $total += $size; + die "XLSX ZIP total size exceeds limit\n" if $total > $MAX_TOTAL_SIZE; + } + return $zip; } +# readChunk() limits compressed input, not inflated output. Small input chunks +# bound temporary inflate buffers; check output before appending it. Never trust +# central-directory sizes alone: actual output must not exceed the declared size. +sub __member_contents { + my ($member) = @_; + my $declared = $member->uncompressedSize(); + die "XLSX ZIP member size exceeds limit\n" if $declared > $MAX_MEMBER_SIZE; + my $old_method = $member->desiredCompressionMethod(Archive::Zip::COMPRESSION_STORED); + my $contents = ''; + my $ok = eval { + die "Cannot read XLSX ZIP member\n" unless $member->rewindData() == Archive::Zip::AZ_OK; + while (1) { + my ($buffer, $status) = $member->readChunk(1024); + die "Cannot read XLSX ZIP member\n" + unless $status == Archive::Zip::AZ_OK || $status == Archive::Zip::AZ_STREAM_END; + my $size = length($contents) + length($$buffer); + die "XLSX ZIP member size exceeds limit\n" if $size > $MAX_MEMBER_SIZE; + die "XLSX ZIP member size does not match metadata\n" if $size > $declared; + $contents .= $$buffer; + last if $status == Archive::Zip::AZ_STREAM_END; + } + die "XLSX ZIP member size does not match metadata\n" if length($contents) != $declared; + 1; + }; + my $error = $@; + $member->endRead(); + $member->desiredCompressionMethod($old_method); + die $error unless $ok; + return $contents; +} + 1; __END__ @@ -328,6 +384,22 @@ =head1 DESCRIPTION +ZIP input is limited to 10,000 entries, 64 MiB per uncompressed member, and +256 MiB of total declared uncompressed data, including unused members. +Members used by the parser are decompressed incrementally and their actual +sizes are checked against the limits and ZIP metadata. Exceeding a limit or +encountering inconsistent sizes raises an exception. + +Applications needing different limits can scope them to a parsing operation: + + local $Spreadsheet::XLSX::MAX_MEMBER_SIZE = 128 * 1024 * 1024; + local $Spreadsheet::XLSX::MAX_TOTAL_SIZE = 512 * 1024 * 1024; + local $Spreadsheet::XLSX::MAX_MEMBERS = 20000; + my $excel = Spreadsheet::XLSX->new($filename); + +These limits bound uncompressed input, not the total memory used by parsed +cells and XML processing. + This module is a (quick and dirty) emulation of L for Excel 2007 (.xlsx) file format. It supports styles and many of Excel's quirks, but not all. It populates the classes from L for interoperability; @@ -339,7 +411,7 @@ =item L -This module (Spradsheet::XLSX) has some serious issues with the way it uses regexs for parsing the XML. +This module (Spreadsheet::XLSX) has some serious issues with the way it uses regexs for parsing the XML. I would strongly encourage switching to L which takes a more reliable approach. =item L, L diff -Nru libspreadsheet-xlsx-perl-0.18/t/cell_references.t libspreadsheet-xlsx-perl-0.20/t/cell_references.t --- libspreadsheet-xlsx-perl-0.18/t/cell_references.t 1970-01-01 00:00:00.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/t/cell_references.t 2026-10-29 20:57:49.000000000 +0000 @@ -0,0 +1,42 @@ +use strict; +use warnings; +use Test::More; +use Archive::Zip qw(:ERROR_CODES); +use File::Temp qw(tempfile); +use Spreadsheet::XLSX; + +sub parse_reference { + my ($reference) = @_; + my %members = ( + 'xl/workbook.xml' => '', + 'xl/_rels/workbook.xml.rels' => '', + 'xl/worksheets/sheet1.xml' => '12', + ); + my ($fh, $filename) = tempfile(SUFFIX => '.xlsx', UNLINK => 1); + close $fh; + my $zip = Archive::Zip->new; + $zip->addString($members{$_}, $_) for keys %members; + die 'Cannot create test workbook' unless $zip->writeToFileNamed($filename) == AZ_OK; + return Spreadsheet::XLSX->new($filename); +} + +for my $reference ('A0', 'A1048577', 'A9999999', 'A' . ('9' x 1000), + 'A01', 'A-1', 'A1junk', 'XFE1', 'AAAA1', '', undef) { + my $workbook = eval { parse_reference($reference) }; + my $error = $@; + ok(!$workbook, 'invalid reference rejects workbook'); + like($error, qr/(?:Invalid XLSX cell reference|XLSX cell (?:row|column) exceeds worksheet limit)/, + 'controlled validation error before cell allocation'); +} + +for my $case (['A1', 0, 0], ['ZZ1', 0, 701], ['AAA1', 0, 702], + ['XFD1', 0, 16383], ['A1048576', 1048575, 0]) { + my ($reference, $row, $col) = @$case; + my $workbook = parse_reference($reference); + is($workbook->{Worksheet}[0]{Cells}[$row][$col]->value(), '2', + "valid reference $reference is decoded correctly"); +} + +done_testing(); diff -Nru libspreadsheet-xlsx-perl-0.18/t/date_limits.t libspreadsheet-xlsx-perl-0.20/t/date_limits.t --- libspreadsheet-xlsx-perl-0.18/t/date_limits.t 1970-01-01 00:00:00.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/t/date_limits.t 2026-10-29 21:01:59.000000000 +0000 @@ -0,0 +1,41 @@ +use strict; +use warnings; +use Test::More; +use Archive::Zip qw(:ERROR_CODES); +use File::Temp qw(tempfile); +use Spreadsheet::XLSX; +use Spreadsheet::XLSX::Utility2007 qw(ExcelFmt ExcelLocaltime); + +for my $epoch (0, 1) { + my $limit = $epoch ? 2957004 : 2958466; + for my $value ($limit, $limit + 1, '1e+100', '1e+9999', -1, + 'NaN', 'Inf', 'not a number', undef) { + my $ok = eval { ExcelLocaltime($value, $epoch); 1 }; + ok(!$ok, "invalid date rejected for epoch $epoch"); + like($@, qr/Invalid or out-of-range XLSX date value/, 'controlled error'); + } + is(ExcelFmt('yyyy-mm-dd', $limit - 1, $epoch), '9999-12-31', + "last supported date for epoch $epoch"); + is(ExcelFmt('yyyy-mm-dd hh:mm:ss', $limit - 0.5, $epoch), + '9999-12-31 12:00:00', 'fractional time on last day is supported'); +} +is(ExcelFmt('yyyy-mm-dd', 0, 1), '1904-01-01', '1904 epoch unchanged'); +is(ExcelFmt('yyyy-mm-dd', 60, 0), '1900-02-29', 'Excel leap-year compatibility'); +is(ExcelFmt('yyyy-mm-dd', 61, 0), '1900-03-01', 'date after Excel leap day'); + +# Verify that an extreme numeric cell is rejected through the public parser. +my %members = ( + 'xl/workbook.xml' => '', + 'xl/_rels/workbook.xml.rels' => '', + 'xl/styles.xml' => '', + 'xl/worksheets/sheet1.xml' => '1e+100', +); +my ($fh, $filename) = tempfile(SUFFIX => '.xlsx', UNLINK => 1); +close $fh; +my $zip = Archive::Zip->new; +$zip->addString($members{$_}, $_) for keys %members; +is($zip->writeToFileNamed($filename), AZ_OK, 'created date regression workbook'); +my $ok = eval { Spreadsheet::XLSX->new($filename); 1 }; +ok(!$ok, 'parser rejects extreme date cell'); +like($@, qr/Invalid or out-of-range XLSX date value/, 'parser propagates controlled error'); +done_testing(); diff -Nru libspreadsheet-xlsx-perl-0.18/t/format_conditions.t libspreadsheet-xlsx-perl-0.20/t/format_conditions.t --- libspreadsheet-xlsx-perl-0.18/t/format_conditions.t 1970-01-01 00:00:00.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/t/format_conditions.t 2026-10-29 20:46:39.000000000 +0000 @@ -0,0 +1,48 @@ +use strict; +use warnings; +use Test::More; +use Archive::Zip qw(:ERROR_CODES); +use File::Temp qw(tempfile); +use Spreadsheet::XLSX; +use Spreadsheet::XLSX::Utility2007 qw(ExcelFmt); + +for my $case ( + ['>2', 3, 1], ['>2', 2, 0], ['>=2', 2, 1], ['>=2', 1, 0], + ['<2', 1, 1], ['<2', 2, 0], ['<=2', 2, 1], ['<=2', 3, 0], + ['=2', 2, 1], ['=2', 3, 0], ['==2', 2, 1], ['==2', 3, 0], + ['<>2', 3, 1], ['<>2', 2, 0], ['> -1.5', -1, 1], + ['>=.5', '0.5', 1], ['=1.', 1, 1], ['=+1e2', 100, 1], + ['=1E-2', '0.01', 1], +) { + my ($condition, $value, $matches) = @$case; + is(ExcelFmt("[$condition]0.0;0.00", $value), + sprintf($matches ? '%.1f' : '%.2f', $value), "$value compared with $condition"); +} + +our $executed = 0; +my $payload = '>0 and do { $main::executed = 1 }'; +for my $condition ($payload, '>0 trailing', '=>1', '>1e', '>NaN', '>1\n') { + is(ExcelFmt("[$condition]0.0;0.00", 1), '1.00', + "invalid condition uses fallback: $condition"); +} +is($executed, 0, 'format condition cannot execute Perl'); +ExcelFmt('[>0]"yes";"no"', '1"; $main::executed = 1; #'); +is($executed, 0, 'cell value cannot execute Perl'); + +# Exercise the public parser path with a crafted workbook, without OS commands. +my %members = ( + 'xl/workbook.xml' => '', + 'xl/_rels/workbook.xml.rels' => '', + 'xl/styles.xml' => qq{}, + 'xl/worksheets/sheet1.xml' => '1', +); +my ($fh, $filename) = tempfile(SUFFIX => '.xlsx', UNLINK => 1); +close $fh; +my $zip = Archive::Zip->new; +$zip->addString($members{$_}, $_) for keys %members; +is($zip->writeToFileNamed($filename), AZ_OK, 'created regression workbook'); +my $workbook = Spreadsheet::XLSX->new($filename); +ok($workbook, 'crafted workbook parses'); +is($executed, 0, 'parsing workbook cannot execute format condition'); +is($workbook->{Worksheet}[0]{Cells}[0][0]->value(), '1', 'fallback formats value'); +done_testing(); diff -Nru libspreadsheet-xlsx-perl-0.18/t/number_format_ids.t libspreadsheet-xlsx-perl-0.20/t/number_format_ids.t --- libspreadsheet-xlsx-perl-0.18/t/number_format_ids.t 1970-01-01 00:00:00.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/t/number_format_ids.t 2026-10-29 21:03:29.000000000 +0000 @@ -0,0 +1,45 @@ +use strict; +use warnings; +use Test::More; +use Archive::Zip qw(:ERROR_CODES); +use File::Temp qw(tempfile); +use Spreadsheet::XLSX; + +sub parse_style { + my ($reference, $declaration) = @_; + my %members = ( + 'xl/workbook.xml' => '', + 'xl/_rels/workbook.xml.rels' => '', + 'xl/styles.xml' => '' + . (defined($declaration) ? qq{} : '') + . qq{}, + 'xl/worksheets/sheet1.xml' => '2', + ); + my ($fh, $filename) = tempfile(SUFFIX => '.xlsx', UNLINK => 1); + close $fh; + my $zip = Archive::Zip->new; + $zip->addString($members{$_}, $_) for keys %members; + die 'Cannot create test workbook' unless $zip->writeToFileNamed($filename) == AZ_OK; + return Spreadsheet::XLSX->new($filename); +} + +for my $id ('(', '.*', '164|14', '(a+)+$', '[0-9]+', '', '-1', '1.5', + ' 164', '164junk', '1e2') { + for my $location ('reference', 'declaration') { + my $ok = eval { + $location eq 'reference' ? parse_style($id, '164') : parse_style('164', $id); + 1; + }; + ok(!$ok, "reject invalid $location ID: $id"); + like($@, qr/\AInvalid XLSX number format ID\n\z/, + 'controlled validation error instead of regex compilation'); + } +} + +is(parse_style('164', '164')->{Worksheet}[0]{Cells}[0][0]->value(), + '2.000', 'custom numeric format ID still resolves'); +is(parse_style('0', undef)->{Worksheet}[0]{Cells}[0][0]->value(), + '2', 'default numeric format ID still resolves'); +is(parse_style('14', undef)->{Worksheet}[0]{Cells}[0][0]->value(), + '1900-01-02', 'built-in date format ID still resolves'); +done_testing(); diff -Nru libspreadsheet-xlsx-perl-0.18/t/zip_limits.t libspreadsheet-xlsx-perl-0.20/t/zip_limits.t --- libspreadsheet-xlsx-perl-0.18/t/zip_limits.t 1970-01-01 00:00:00.000000000 +0000 +++ libspreadsheet-xlsx-perl-0.20/t/zip_limits.t 2026-10-29 21:06:23.000000000 +0000 @@ -0,0 +1,118 @@ +use strict; +use warnings; +use Test::More; +use Archive::Zip qw(:ERROR_CODES :CONSTANTS); +use File::Temp qw(tempfile); +use Spreadsheet::XLSX; + +my %base = ( + 'xl/workbook.xml' => '', + 'xl/_rels/workbook.xml.rels' => '', + 'xl/worksheets/sheet1.xml' => '2', + 'xl/sharedStrings.xml' => 'hello', + 'xl/styles.xml' => '', +); + +sub archive { + my ($members, $stored) = @_; + my ($fh, $filename) = tempfile(SUFFIX => '.xlsx', UNLINK => 1); + close $fh; + my $zip = Archive::Zip->new; + for my $name (sort keys %$members) { + my $member = $zip->addString($members->{$name}, $name); + $member->desiredCompressionMethod($stored ? COMPRESSION_STORED : COMPRESSION_DEFLATED); + } + die 'Cannot create test archive' unless $zip->writeToFileNamed($filename) == AZ_OK; + return $filename; +} + +sub rejects { + my ($filename, $pattern, $description) = @_; + my $ok = eval { Spreadsheet::XLSX->new($filename); 1 }; + my $error = $@; + ok(!$ok, $description); + like($error, $pattern, 'controlled ZIP validation error'); +} + +for my $stored (0, 1) { + my $filename = archive(\%base, $stored); + is(Spreadsheet::XLSX->new($filename)->{Worksheet}[0]{Cells}[0][0]->value(), + '2', 'ordinary workbook parses'); + open my $fh, '<', $filename or die $!; + binmode $fh; + is(Spreadsheet::XLSX->new($fh)->{Worksheet}[0]{Cells}[0][0]->value(), + '2', 'filehandle input parses'); + close $fh; +} + +my $filename = archive(\%base); +{ + local $Spreadsheet::XLSX::MAX_MEMBERS = scalar(keys %base) - 1; + rejects($filename, qr/entry count exceeds limit/, 'too many entries rejected'); +} +my $total = 0; +my $largest = 0; +for my $data (values %base) { + $total += length($data); + $largest = length($data) if length($data) > $largest; +} +{ + local $Spreadsheet::XLSX::MAX_MEMBER_SIZE = $largest; + local $Spreadsheet::XLSX::MAX_TOTAL_SIZE = $total; + local $Spreadsheet::XLSX::MAX_MEMBERS = scalar(keys %base); + ok(Spreadsheet::XLSX->new($filename), 'exact limits accepted'); +} +{ + local $Spreadsheet::XLSX::MAX_TOTAL_SIZE = $total - 1; + rejects($filename, qr/total size exceeds limit/, 'aggregate size rejected'); +} +{ + local $Spreadsheet::XLSX::MAX_MEMBER_SIZE = 1024; + my %members = (%base, 'unused.bin' => 'x' x 8192); + rejects(archive(\%members), qr/member size exceeds limit/, + 'highly compressed unused member is included in preflight'); +} + +# Change only the central-directory size, leaving compressed data intact. +sub forge_size { + my ($filename, $target, $size) = @_; + open my $fh, '<', $filename or die $!; + binmode $fh; + my $bytes = do { local $/; <$fh> }; + close $fh; + my $offset = 0; + my $found = 0; + while (($offset = index($bytes, "PK\x01\x02", $offset)) >= 0) { + my ($name_length, $extra_length, $comment_length) = + unpack('vvv', substr($bytes, $offset + 28, 6)); + if (substr($bytes, $offset + 46, $name_length) eq $target) { + substr($bytes, $offset + 24, 4) = pack('V', $size); + $found = 1; + last; + } + $offset += 46 + $name_length + $extra_length + $comment_length; + } + die 'Missing central-directory entry' unless $found; + open $fh, '>', $filename or die $!; + binmode $fh; + print {$fh} $bytes; + close $fh; +} + +for my $name (sort keys %base) { + for my $delta (-1, 1) { + my $forged = archive(\%base); + forge_size($forged, $name, length($base{$name}) + $delta); + rejects($forged, qr/member size does not match metadata/, + "actual size checked for $name (delta $delta)"); + } +} +{ + local $Spreadsheet::XLSX::MAX_MEMBER_SIZE = 1024; + my %members = (%base, 'xl/sharedStrings.xml' => '' . ('x' x 8192) . ''); + my $forged = archive(\%members); + forge_size($forged, 'xl/sharedStrings.xml', 100); + rejects($forged, qr/member size exceeds limit/, 'actual inflated bytes enforce limit'); +} + +done_testing();