Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package perl-Spreadsheet-XLSX for
openSUSE:Factory checked in at 2026-10-30 18:18:27
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/perl-Spreadsheet-XLSX (Old)
and /work/SRC/openSUSE:Factory/.perl-Spreadsheet-XLSX.new.1255 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "perl-Spreadsheet-XLSX"
Fri Oct 30 18:18:27 2026 rev:13 rq:1393888 version:0.200.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/perl-Spreadsheet-XLSX/perl-Spreadsheet-XLSX.changes
2024-04-07 22:13:44.759610253 +0200
+++
/work/SRC/openSUSE:Factory/.perl-Spreadsheet-XLSX.new.1255/perl-Spreadsheet-XLSX.changes
2026-10-30 18:24:11.099118302 +0100
@@ -1,0 +2,23 @@
+Fri Oct 30 11:29:45 UTC 2026 - Tina Müller <[email protected]>
+
+- updated to 0.200.0 (0.20)
+ see /usr/share/doc/packages/perl-Spreadsheet-XLSX/Changes
+
+ 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.
+ CVE-2026-115943 bsc#1288922
+ Prevents arbitrary Perl code execution when parsing crafted XLSX files.
+ - Add regression tests for numeric conditions and malicious XLSX formats.
+
+-------------------------------------------------------------------
Old:
----
Spreadsheet-XLSX-0.18.tar.gz
New:
----
README.md
Spreadsheet-XLSX-0.20.tar.gz
_scmsync.obsinfo
build.specials.obscpio
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ perl-Spreadsheet-XLSX.spec ++++++
--- /var/tmp/diff_new_pack.MxeWW3/_old 2026-10-30 18:24:12.078159287 +0100
+++ /var/tmp/diff_new_pack.MxeWW3/_new 2026-10-30 18:24:12.080159371 +0100
@@ -1,7 +1,7 @@
#
# spec file for package perl-Spreadsheet-XLSX
#
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,23 +18,24 @@
%define cpan_name Spreadsheet-XLSX
Name: perl-Spreadsheet-XLSX
-Version: 0.180.0
+Version: 0.200.0
Release: 0
-# 0.18 -> normalize -> 0.180.0
-%define cpan_version 0.18
+# 0.20 -> normalize -> 0.200.0
+%define cpan_version 0.20
License: Artistic-1.0 OR GPL-1.0-or-later
Summary: Perl extension for reading MS Excel 2007 files
URL: https://metacpan.org/release/%{cpan_name}
Source0:
https://cpan.metacpan.org/authors/id/A/AS/ASB/%{cpan_name}-%{cpan_version}.tar.gz
Source1: cpanspec.yml
+Source100: README.md
BuildArch: noarch
BuildRequires: perl
BuildRequires: perl-macros
-BuildRequires: perl(Archive::Zip) >= 1.18
+BuildRequires: perl(Archive::Zip) >= 1.180
BuildRequires: perl(Spreadsheet::ParseExcel)
BuildRequires: perl(Test::NoWarnings)
BuildRequires: perl(Test::Warnings)
-Requires: perl(Archive::Zip) >= 1.18
+Requires: perl(Archive::Zip) >= 1.180
Requires: perl(Spreadsheet::ParseExcel)
Provides: perl(Spreadsheet::XLSX) = %{version}
Provides: perl(Spreadsheet::XLSX::Fmt2007) = %{version}
@@ -43,13 +44,30 @@
%{perl_requires}
%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 Spreadsheet::ParseExcel for
Excel 2007 (.xlsx) file format. It supports styles and many of Excel's
quirks, but not all. It populates the classes from Spreadsheet::ParseExcel
for interoperability; including Workbook, Worksheet, and Cell.
%prep
-%autosetup -n %{cpan_name}-%{cpan_version}
+%autosetup -n %{cpan_name}-%{cpan_version} -p1
%build
perl Makefile.PL INSTALLDIRS=vendor
++++++ README.md ++++++
## Build Results
Current state of perl in openSUSE:Factory is

The current state of perl in the devel project build (devel:languages:perl)

++++++ Spreadsheet-XLSX-0.18.tar.gz -> Spreadsheet-XLSX-0.20.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/Spreadsheet-XLSX-0.18/Changes
new/Spreadsheet-XLSX-0.20/Changes
--- old/Spreadsheet-XLSX-0.18/Changes 2024-03-09 21:08:39.000000000 +0100
+++ new/Spreadsheet-XLSX-0.20/Changes 2026-10-29 22:18:37.000000000 +0100
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/Spreadsheet-XLSX-0.18/MANIFEST
new/Spreadsheet-XLSX-0.20/MANIFEST
--- old/Spreadsheet-XLSX-0.18/MANIFEST 2023-12-31 14:23:06.000000000 +0100
+++ new/Spreadsheet-XLSX-0.20/MANIFEST 2026-10-29 22:13:04.000000000 +0100
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/Spreadsheet-XLSX-0.18/META.json
new/Spreadsheet-XLSX-0.20/META.json
--- old/Spreadsheet-XLSX-0.18/META.json 2024-03-09 21:13:23.000000000 +0100
+++ new/Spreadsheet-XLSX-0.20/META.json 2026-10-29 22:20:02.000000000 +0100
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/Spreadsheet-XLSX-0.18/META.yml
new/Spreadsheet-XLSX-0.20/META.yml
--- old/Spreadsheet-XLSX-0.18/META.yml 2024-03-09 21:13:21.000000000 +0100
+++ new/Spreadsheet-XLSX-0.20/META.yml 2026-10-29 22:20:00.000000000 +0100
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/Spreadsheet-XLSX-0.18/README
new/Spreadsheet-XLSX-0.20/README
--- old/Spreadsheet-XLSX-0.18/README 2023-12-31 14:22:41.000000000 +0100
+++ new/Spreadsheet-XLSX-0.20/README 2026-10-29 22:17:37.000000000 +0100
@@ -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-<version>.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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/Spreadsheet-XLSX-0.18/cpanfile
new/Spreadsheet-XLSX-0.20/cpanfile
--- old/Spreadsheet-XLSX-0.18/cpanfile 1970-01-01 01:00:00.000000000 +0100
+++ new/Spreadsheet-XLSX-0.20/cpanfile 2026-10-29 22:13:04.000000000 +0100
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/Spreadsheet-XLSX-0.18/lib/Spreadsheet/XLSX/Fmt2007.pm
new/Spreadsheet-XLSX-0.20/lib/Spreadsheet/XLSX/Fmt2007.pm
--- old/Spreadsheet-XLSX-0.18/lib/Spreadsheet/XLSX/Fmt2007.pm 2024-03-09
21:12:16.000000000 +0100
+++ new/Spreadsheet-XLSX-0.20/lib/Spreadsheet/XLSX/Fmt2007.pm 2026-10-29
22:09:41.000000000 +0100
@@ -3,7 +3,7 @@
use warnings;
use Spreadsheet::XLSX::Utility2007 qw(ExcelFmt);
-our $VERSION = '0.18'; #
+our $VERSION = '0.20'; #
my %hFmtDefault = (
0x00 => '@',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/Spreadsheet-XLSX-0.18/lib/Spreadsheet/XLSX/Utility2007.pm
new/Spreadsheet-XLSX-0.20/lib/Spreadsheet/XLSX/Utility2007.pm
--- old/Spreadsheet-XLSX-0.18/lib/Spreadsheet/XLSX/Utility2007.pm
2024-03-09 21:12:22.000000000 +0100
+++ new/Spreadsheet-XLSX-0.20/lib/Spreadsheet/XLSX/Utility2007.pm
2026-10-29 22:09:35.000000000 +0100
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/Spreadsheet-XLSX-0.18/lib/Spreadsheet/XLSX.pm
new/Spreadsheet-XLSX-0.20/lib/Spreadsheet/XLSX.pm
--- old/Spreadsheet-XLSX-0.18/lib/Spreadsheet/XLSX.pm 2024-03-09
21:12:01.000000000 +0100
+++ new/Spreadsheet-XLSX-0.20/lib/Spreadsheet/XLSX.pm 2026-10-29
22:09:26.000000000 +0100
@@ -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 (/^\<c\s*.*?\s*r=\"([A-Z])([A-Z]?)(\d+)\"/) {
+ my $sheet_xml = __member_contents($member_sheet);
+ foreach ($sheet_xml =~ /(\<.*?\/?\>|.*?(?=\<))/g) {
+ if (/^<c(?:\s|\/?>)/) {
- ($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\/>/<t><\/t>/gsm; # this handles an empty t tag in the
xml <t/>
foreach my $si ($mstr =~ /<si.*?>(.*?)<\/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 =~ /\<Relationship (.*?)\/?\>/g) {
+ my $rels_xml = __member_contents($member_rels);
+ foreach ($rels_xml =~ /\<Relationship (.*?)\/?\>/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<Spreadsheet::ParseExcel> for
Excel 2007 (.xlsx) file format. It supports styles and many of Excel's
quirks,
but not all. It populates the classes from L<Spreadsheet::ParseExcel> for
interoperability;
@@ -339,7 +411,7 @@
=item L<Spreadsheet::ParseXLSX>
-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<Spreadsheet::ParseXLSX> which takes
a more reliable approach.
=item L<Text::CSV_XS>, L<Text::CSV_PP>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/Spreadsheet-XLSX-0.18/t/cell_references.t
new/Spreadsheet-XLSX-0.20/t/cell_references.t
--- old/Spreadsheet-XLSX-0.18/t/cell_references.t 1970-01-01
01:00:00.000000000 +0100
+++ new/Spreadsheet-XLSX-0.20/t/cell_references.t 2026-10-29
21:57:49.000000000 +0100
@@ -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' => '<workbook xmlns:r="x"><sheets><sheet name="S1"
sheetId="1" r:id="rId1"/></sheets></workbook>',
+ 'xl/_rels/workbook.xml.rels' => '<Relationships><Relationship
Id="rId1" Target="worksheets/sheet1.xml"/></Relationships>',
+ 'xl/worksheets/sheet1.xml' => '<worksheet><sheetData><row><c
r="A1"><v>1</v></c><c '
+ . (defined($reference) ? qq{r="$reference"} : '')
+ . '><v>2</v></c></row></sheetData></worksheet>',
+ );
+ 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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/Spreadsheet-XLSX-0.18/t/date_limits.t
new/Spreadsheet-XLSX-0.20/t/date_limits.t
--- old/Spreadsheet-XLSX-0.18/t/date_limits.t 1970-01-01 01:00:00.000000000
+0100
+++ new/Spreadsheet-XLSX-0.20/t/date_limits.t 2026-10-29 22:01:59.000000000
+0100
@@ -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' => '<workbook xmlns:r="x"><sheets><sheet name="S1"
sheetId="1" r:id="rId1"/></sheets></workbook>',
+ 'xl/_rels/workbook.xml.rels' => '<Relationships><Relationship Id="rId1"
Target="worksheets/sheet1.xml"/></Relationships>',
+ 'xl/styles.xml' => '<styleSheet><cellXfs><xf
numFmtId="14"/></cellXfs></styleSheet>',
+ 'xl/worksheets/sheet1.xml' => '<worksheet><sheetData><row><c r="A1"
s="0"><v>1e+100</v></c></row></sheetData></worksheet>',
+);
+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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/Spreadsheet-XLSX-0.18/t/format_conditions.t
new/Spreadsheet-XLSX-0.20/t/format_conditions.t
--- old/Spreadsheet-XLSX-0.18/t/format_conditions.t 1970-01-01
01:00:00.000000000 +0100
+++ new/Spreadsheet-XLSX-0.20/t/format_conditions.t 2026-10-29
21:46:39.000000000 +0100
@@ -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' => '<workbook xmlns:r="x"><sheets><sheet name="S1"
sheetId="1" r:id="rId1"/></sheets></workbook>',
+ 'xl/_rels/workbook.xml.rels' => '<Relationships><Relationship Id="rId1"
Target="worksheets/sheet1.xml"/></Relationships>',
+ 'xl/styles.xml' => qq{<styleSheet><numFmts count="1"><numFmt
numFmtId="164" formatCode="[$payload]0;0"/></numFmts><cellXfs count="1"><xf
numFmtId="164" fontId="0" fillId="0" borderId="0"/></cellXfs></styleSheet>},
+ 'xl/worksheets/sheet1.xml' => '<worksheet><sheetData><row r="1"><c r="A1"
s="0"><v>1</v></c></row></sheetData></worksheet>',
+);
+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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/Spreadsheet-XLSX-0.18/t/number_format_ids.t
new/Spreadsheet-XLSX-0.20/t/number_format_ids.t
--- old/Spreadsheet-XLSX-0.18/t/number_format_ids.t 1970-01-01
01:00:00.000000000 +0100
+++ new/Spreadsheet-XLSX-0.20/t/number_format_ids.t 2026-10-29
22:03:29.000000000 +0100
@@ -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' => '<workbook xmlns:r="x"><sheets><sheet name="S1"
sheetId="1" r:id="rId1"/></sheets></workbook>',
+ 'xl/_rels/workbook.xml.rels' => '<Relationships><Relationship
Id="rId1" Target="worksheets/sheet1.xml"/></Relationships>',
+ 'xl/styles.xml' => '<styleSheet>'
+ . (defined($declaration) ? qq{<numFmts><numFmt
numFmtId="$declaration" formatCode="0.000"/></numFmts>} : '')
+ . qq{<cellXfs><xf numFmtId="$reference"/></cellXfs></styleSheet>},
+ 'xl/worksheets/sheet1.xml' => '<worksheet><sheetData><row><c r="A1"
s="0"><v>2</v></c></row></sheetData></worksheet>',
+ );
+ 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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/Spreadsheet-XLSX-0.18/t/zip_limits.t
new/Spreadsheet-XLSX-0.20/t/zip_limits.t
--- old/Spreadsheet-XLSX-0.18/t/zip_limits.t 1970-01-01 01:00:00.000000000
+0100
+++ new/Spreadsheet-XLSX-0.20/t/zip_limits.t 2026-10-29 22:06:23.000000000
+0100
@@ -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' => '<workbook xmlns:r="x"><sheets><sheet name="S1"
sheetId="1" r:id="rId1"/></sheets></workbook>',
+ 'xl/_rels/workbook.xml.rels' => '<Relationships><Relationship Id="rId1"
Target="worksheets/sheet1.xml"/></Relationships>',
+ 'xl/worksheets/sheet1.xml' => '<worksheet><sheetData><row><c
r="A1"><v>2</v></c></row></sheetData></worksheet>',
+ 'xl/sharedStrings.xml' => '<sst><si><t>hello</t></si></sst>',
+ 'xl/styles.xml' => '<styleSheet><cellXfs><xf
numFmtId="0"/></cellXfs></styleSheet>',
+);
+
+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' => '<sst>' . ('x' x 8192) .
'</sst>');
+ 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();
++++++ _scmsync.obsinfo ++++++
mtime: 1793364268
commit: 58eadced768a0421dbb90cba9530c2860590f13c647fef25547893f355511b8e
url: https://src.opensuse.org/perl/perl-Spreadsheet-XLSX
revision: 58eadced768a0421dbb90cba9530c2860590f13c647fef25547893f355511b8e
projectscmsync: https://src.opensuse.org/perl/_ObsPrj
++++++ 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-10-30 13:44:28.000000000 +0100
@@ -0,0 +1 @@
+.osc