From 8d2b88a521fe4e71fe8feff7dbce305b0da3cb26 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:39:44 -0800 Subject: [PATCH] Security Patch --- src/PhpSpreadsheet/Reader/Gnumeric.php | 7 +- src/PhpSpreadsheet/Reader/Ods.php | 67 +++++++-- .../Reader/Gnumeric/Secf5w4Test.php | 31 ++++ .../Reader/Ods/LongStringTest.php | 24 +++ .../Reader/Ods/MetadataBoundsShortTest.php | 142 ++++++++++++++++++ tests/data/Reader/Gnumeric/secf5w4.gnumeric | 17 +++ tests/data/Reader/Ods/long.string | Bin 0 -> 8343 bytes 7 files changed, 270 insertions(+), 18 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Gnumeric/Secf5w4Test.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/LongStringTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/MetadataBoundsShortTest.php create mode 100644 tests/data/Reader/Gnumeric/secf5w4.gnumeric create mode 100644 tests/data/Reader/Ods/long.string diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index 849c549ba..3947aea9e 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader; +use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\DefinedName; @@ -472,11 +473,13 @@ class Gnumeric extends BaseReader $rowHeight = (float) $rowAttributes['Unit']; $hidden = (isset($rowAttributes['Hidden'])) && ((string) $rowAttributes['Hidden'] == '1'); $rowCount = (int) ($rowAttributes['Count'] ?? 1); - while ($whichRow < $row) { + $limit = min($row, AddressRange::MAX_ROW); + while ($whichRow < $limit) { ++$whichRow; $this->setRowHeight($whichRow, $defaultHeight); } - while (($whichRow < ($row + $rowCount)) && ($whichRow < $maxRow)) { + $limit = min($maxRow, AddressRange::MAX_ROW); + while (($whichRow < ($row + $rowCount)) && ($whichRow < $limit)) { ++$whichRow; $this->setRowHeight($whichRow, $rowHeight); if ($hidden) { diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index aa5e98c79..8c7f3669d 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader; +use Composer\Pcre\Preg; use DOMAttr; use DOMDocument; use DOMElement; @@ -258,13 +259,22 @@ class Ods extends BaseReader public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet { File::assertFile($filename, self::INITIAL_FILE); - $zip = new ZipArchive(); - $zip->open($filename); + try { + $zip->open($filename); + + return $this->load2($filename, $spreadsheet, $zip); + } finally { + $zip->close(); + } + } + + public function load2(string $filename, Spreadsheet $spreadsheet, ZipArchive $zip): Spreadsheet + { $xml = @$this->loadZipSimple($zip, 'meta.xml'); if ($xml === false) { - throw new Exception('Unable to read data from {$pFilename}'); + throw new Exception('Unable to read data from {$filename}'); } $namespacesMeta = $xml->getNamespaces(true); @@ -449,16 +459,20 @@ class Ods extends BaseReader if ($cellData->hasAttributeNS($tableNs, 'number-matrix-columns-spanned')) { if ($cellData->hasAttributeNS($tableNs, 'number-matrix-rows-spanned')) { $cellDataType = 'array'; - $arrayRow = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-rows-spanned'); - $arrayCol = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-columns-spanned'); + $arrayRow = self::boundedInteger( + $cellData->getAttributeNS($tableNs, 'number-matrix-rows-spanned'), + 1, + AddressRange::MAX_ROW - $rowID + 1, + 'number-matrix-rows-spanned' + ); + $arrayCol = self::boundedInteger( + $cellData->getAttributeNS($tableNs, 'number-matrix-columns-spanned'), + 1, + AddressRange::MAX_COLUMN_INT - Coordinate::columnIndexFromString($columnID) + 1, + 'number-matrix-columns-spanned' + ); $lastRow = $rowID + $arrayRow - 1; - $lastCol = $columnID; - while ($arrayCol > 1) { - StringHelper::stringIncrement( - $lastCol - ); - --$arrayCol; - } + $lastCol = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($columnID) + $arrayCol - 1); $cellDataRef = "$columnID$rowID:$lastCol$lastRow"; } } @@ -701,8 +715,6 @@ class Ods extends BaseReader if ($zip->locateName('settings.xml') !== false) { $this->processSettings($zip, $spreadsheet); } - $zip->close(); - unset($this->zip); // Return return $spreadsheet; @@ -802,13 +814,13 @@ class Ods extends BaseReader } } - return $str; + return mb_substr($str, 0, DataType::MAX_STRING_LENGTH); } private static function getMultiplier(?DOMAttr $cAttr): int { if ($cAttr) { - $multiplier = (int) $cAttr->nodeValue; + $multiplier = max(1, min((int) $cAttr->nodeValue, DataType::MAX_STRING_LENGTH)); } else { $multiplier = 1; } @@ -963,4 +975,27 @@ class Ods extends BaseReader return (bool) $retVal; } + + /** Validate the lexical value and its bound before converting to an integer. */ + private static function boundedInteger(string $value, int $minimum, int $maximum, string $attribute): int + { + $value = trim($value); + if (!Preg::isMatch('/^\+?[0-9]+$/D', $value)) { + throw new Exception("Invalid ODS $attribute value."); + } + $digits = ltrim($value, '+0'); + $maximumDigits = (string) $maximum; + if ( + strlen($digits) > strlen($maximumDigits) + || (strlen($digits) === strlen($maximumDigits) && strcmp($digits, $maximumDigits) > 0) + ) { + throw new Exception("Invalid ODS $attribute value."); + } + $integer = (int) $value; + if ($integer < $minimum) { + throw new Exception("Invalid ODS $attribute value."); + } + + return $integer; + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/Secf5w4Test.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/Secf5w4Test.php new file mode 100644 index 000000000..4d4b48b1d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/Secf5w4Test.php @@ -0,0 +1,31 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + $row = AddressRange::MAX_ROW; + self::assertTrue($sheet->rowDimensionExists($row)); + $rowDimension = $sheet->getRowDimension($row); + self::assertEqualsWithDelta(12.0, $rowDimension->getRowHeight(), 1.0e-8); + ++$row; + self::assertFalse($sheet->rowDimensionExists($row)); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/LongStringTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/LongStringTest.php new file mode 100644 index 000000000..1ff40aff1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/LongStringTest.php @@ -0,0 +1,24 @@ +load($infile); + $sheet = $spreadsheet->getActiveSheet(); + $value = $sheet->getCell('A1')->getValue(); + self::assertIsString($value); + self::assertSame(DataType::MAX_STRING_LENGTH, mb_strlen($value)); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/MetadataBoundsShortTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/MetadataBoundsShortTest.php new file mode 100644 index 000000000..4960e03d6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/MetadataBoundsShortTest.php @@ -0,0 +1,142 @@ + */ + private array $files = []; + + protected function tearDown(): void + { + foreach ($this->files as $file) { + unlink($file); + } + } + + public function testReaderRecoversAfterRejectedMatrixSpan(): void + { + $reader = new Ods(); + + try { + $reader->load($this->matrixFixture('2', '1', AddressRange::MAX_COLUMN_INT, 1)); + self::fail('Expected invalid matrix span rejection'); + } catch (ReaderException $exception) { + self::assertSame('Invalid ODS number-matrix-columns-spanned value.', $exception->getMessage()); + } + $spreadsheet = $reader->load($this->matrixFixture('3', '2', 2, 2)); + + try { + $cell = $spreadsheet->getActiveSheet()->getCell('B2'); + self::assertSame('=1', $cell->getValue()); + self::assertSame(['t' => 'array', 'ref' => 'B2:D3'], $cell->getFormulaAttributes()); + self::assertSame(1, $cell->getOldCalculatedValue()); + } finally { + $spreadsheet->disconnectWorksheets(); + } + } + + /** @return array */ + public static function invalidMatrices(): array + { + $cases = []; + foreach (['0', '-1', '', '1.5', 'abc', '2e9', '2000000000', '999999999999999999999999999999'] as $count) { + $cases['columns ' . $count] = [$count, '1', 1, 1, 'number-matrix-columns-spanned']; + $cases['rows ' . $count] = ['1', $count, 1, 1, 'number-matrix-rows-spanned']; + } + $cases['column past maximum'] = [(string) (AddressRange::MAX_COLUMN_INT + 1), '1', 1, 1, 'number-matrix-columns-spanned']; + $cases['row past maximum'] = ['1', (string) (AddressRange::MAX_ROW + 1), 1, 1, 'number-matrix-rows-spanned']; + $cases['columns from B exceed remaining width'] = [(string) AddressRange::MAX_COLUMN_INT, '1', 2, 1, 'number-matrix-columns-spanned']; + $cases['rows from row 2 exceed remaining height'] = ['1', (string) AddressRange::MAX_ROW, 1, 2, 'number-matrix-rows-spanned']; + $cases['columns from XFD exceed remaining width'] = ['2', '1', AddressRange::MAX_COLUMN_INT, 1, 'number-matrix-columns-spanned']; + $cases['rows from final row exceed remaining height'] = ['1', '2', 1, AddressRange::MAX_ROW, 'number-matrix-rows-spanned']; + + return $cases; + } + + #[DataProvider('invalidMatrices')] + public function testMatrixSpansAreRejectedBeforeIteration(string $columns, string $rows, int $column, int $row, string $attribute): void + { + $this->expectException(ReaderException::class); + $this->expectExceptionMessage('Invalid ODS ' . $attribute . ' value.'); + (new Ods())->load($this->matrixFixture($columns, $rows, $column, $row)); + } + + /** @return array */ + public static function validMatrices(): array + { + return [ + 'single cell' => ['1', '1', 1, 1, 'A1:A1'], + 'small matrix' => ['3', '2', 2, 2, 'B2:D3'], + 'integer lexical forms' => [' +003 ', '0002', 2, 2, 'B2:D3'], + 'full column width' => [(string) AddressRange::MAX_COLUMN_INT, '1', 1, 1, 'A1:XFD1'], + 'full row height' => ['1', (string) AddressRange::MAX_ROW, 1, 1, 'A1:A1048576'], + 'remaining width from B' => [(string) (AddressRange::MAX_COLUMN_INT - 1), '1', 2, 1, 'B1:XFD1'], + 'remaining height from row 2' => ['1', (string) (AddressRange::MAX_ROW - 1), 1, 2, 'A2:A1048576'], + 'bottom right cell' => ['1', '1', AddressRange::MAX_COLUMN_INT, AddressRange::MAX_ROW, 'XFD1048576:XFD1048576'], + ]; + } + + #[DataProvider('validMatrices')] + public function testValidMatrixRangesArePreserved(string $columns, string $rows, int $column, int $row, string $expected): void + { + $spreadsheet = (new Ods())->load($this->matrixFixture($columns, $rows, $column, $row)); + + try { + $start = explode(':', $expected)[0]; + $cell = $spreadsheet->getActiveSheet()->getCell($start); + self::assertSame(['t' => 'array', 'ref' => $expected], $cell->getFormulaAttributes()); + self::assertSame('=1', $cell->getValue()); + } finally { + $spreadsheet->disconnectWorksheets(); + } + } + + private function matrixFixture(string $columns, string $rows, int $column, int $row): string + { + $body = ''; + if ($row > 1) { + $body .= ''; + } + $body .= ''; + if ($column > 1) { + $body .= ''; + } + $body .= '1'; + + return $this->fixture($body); + } + + private function fixture(string $body, string $styles = '', string $automaticStyles = ''): string + { + $file = tempnam(sys_get_temp_dir(), 'ods-bounds-'); + self::assertNotFalse($file); + $this->files[] = $file; + $zip = new ZipArchive(); + self::assertTrue($zip->open($file, ZipArchive::OVERWRITE)); + $ns = 'xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" ' + . 'xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" ' + . 'xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" ' + . 'xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" ' + . 'xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" ' + . 'xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" ' + . 'xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" ' + . 'xmlns:xlink="http://www.w3.org/1999/xlink"'; + $zip->addFromString('mimetype', 'application/vnd.oasis.opendocument.spreadsheet'); + $zip->addFromString('meta.xml', ''); + $zip->addFromString('styles.xml', '' . $styles . ''); + $zip->addFromString('content.xml', '' . $automaticStyles . '' . $body . ''); + self::assertTrue($zip->close()); + + return $file; + } +} diff --git a/tests/data/Reader/Gnumeric/secf5w4.gnumeric b/tests/data/Reader/Gnumeric/secf5w4.gnumeric new file mode 100644 index 000000000..cb2c003eb --- /dev/null +++ b/tests/data/Reader/Gnumeric/secf5w4.gnumeric @@ -0,0 +1,17 @@ + + + + Sheet1 + + + + Sheet1 + 1 + 1 + + + + + + + diff --git a/tests/data/Reader/Ods/long.string b/tests/data/Reader/Ods/long.string new file mode 100644 index 0000000000000000000000000000000000000000..71d3a6e8f3f13fe883a5e73539623a15673213a6 GIT binary patch literal 8343 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 H?&-e(L+)bt literal 0 HcmV?d00001