mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-11-09 11:26:17 +00:00
Security Patch
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Reader\Gnumeric;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Gnumeric;
|
||||
use PHPUnit\Framework\Attributes;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
// All tests must run in separate process due to ini_set
|
||||
#[Attributes\RunTestsInSeparateProcesses]
|
||||
class Secf5w4Test extends TestCase
|
||||
{
|
||||
public function testf5w4(): void
|
||||
{
|
||||
ini_set('memory_limit', '512M');
|
||||
$filename = 'tests/data/Reader/Gnumeric/secf5w4.gnumeric';
|
||||
$reader = new Gnumeric();
|
||||
$spreadsheet = $reader->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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Reader\Ods;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Ods;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class LongStringTest extends TestCase
|
||||
{
|
||||
public function testLongString(): void
|
||||
{
|
||||
$infile = 'tests/data/Reader/Ods/long.string';
|
||||
$reader = new Ods();
|
||||
$spreadsheet = $reader->load($infile);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$value = $sheet->getCell('A1')->getValue();
|
||||
self::assertIsString($value);
|
||||
self::assertSame(DataType::MAX_STRING_LENGTH, mb_strlen($value));
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Reader\Ods;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Ods;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ZipArchive;
|
||||
|
||||
class MetadataBoundsShortTest extends TestCase
|
||||
{
|
||||
/** @var list<string> */
|
||||
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<string, array{string, string, int, int, string}> */
|
||||
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<string, array{string, string, int, int, string}> */
|
||||
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 = '<table:table table:name="S">';
|
||||
if ($row > 1) {
|
||||
$body .= '<table:table-row table:number-rows-repeated="' . ($row - 1) . '"><table:table-cell/></table:table-row>';
|
||||
}
|
||||
$body .= '<table:table-row>';
|
||||
if ($column > 1) {
|
||||
$body .= '<table:table-cell table:number-columns-repeated="' . ($column - 1) . '"/>';
|
||||
}
|
||||
$body .= '<table:table-cell table:formula="of:=1" table:number-matrix-columns-spanned="' . $columns . '" table:number-matrix-rows-spanned="' . $rows . '" office:value-type="float" office:value="1"><text:p>1</text:p></table:table-cell></table:table-row></table:table>';
|
||||
|
||||
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', '<office:document-meta ' . $ns . '><office:meta/></office:document-meta>');
|
||||
$zip->addFromString('styles.xml', '<office:document-styles ' . $ns . '>' . $styles . '</office:document-styles>');
|
||||
$zip->addFromString('content.xml', '<office:document-content ' . $ns . '>' . $automaticStyles . '<office:body><office:spreadsheet>' . $body . '</office:spreadsheet></office:body></office:document-content>');
|
||||
self::assertTrue($zip->close());
|
||||
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gnm:Workbook xmlns:gnm="http://www.gnumeric.org/v10.dtd">
|
||||
<gnm:SheetNameIndex>
|
||||
<gnm:SheetName>Sheet1</gnm:SheetName>
|
||||
</gnm:SheetNameIndex>
|
||||
<gnm:Sheets>
|
||||
<gnm:Sheet>
|
||||
<gnm:Name>Sheet1</gnm:Name>
|
||||
<gnm:MaxCol>1</gnm:MaxCol>
|
||||
<gnm:MaxRow>1</gnm:MaxRow>
|
||||
<gnm:Cells/>
|
||||
<gnm:Rows DefaultSizePts="12.0">
|
||||
<gnm:RowInfo No="5000000" Unit="12.0"/>
|
||||
</gnm:Rows>
|
||||
</gnm:Sheet>
|
||||
</gnm:Sheets>
|
||||
</gnm:Workbook>
|
||||
Binary file not shown.
Reference in New Issue
Block a user