Security Patches

This commit is contained in:
oleibman
2026-09-15 20:57:04 -07:00
parent 784c9b2956
commit 9a0b48622e
15 changed files with 306 additions and 9 deletions
+59 -6
View File
@@ -7,6 +7,7 @@ use DOMDocument;
use DOMElement;
use DOMNode;
use DOMText;
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Helper\Dimension as HelperDimension;
@@ -182,7 +183,11 @@ class Ods extends BaseReader
$xml->read();
if (self::getXmlName($xml) == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) {
$rowspan = $xml->getAttribute('table:number-rows-repeated');
$rowspan = empty($rowspan) ? 1 : $rowspan;
$rowspan = empty($rowspan) ? 1 : (int) $rowspan;
self::checkRowsRepeated(
$tmpInfo['totalRows'],
$rowspan
);
$tmpInfo['totalRows'] += $rowspan;
$tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
$currCells = 0;
@@ -191,13 +196,24 @@ class Ods extends BaseReader
do {
$doread = true;
if (self::getXmlName($xml) == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
$mergeSize = $xml->getAttribute('table:number-columns-repeated');
$mergeSize = empty($mergeSize) ? 1 : (int) $mergeSize;
self::checkColumnsRepeatedInt(
$currCells,
$mergeSize
);
$currCells += (int) $mergeSize;
if (!$xml->isEmptyElement) {
++$currCells;
$xml->next();
$doread = false;
}
} elseif (self::getXmlName($xml) == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
$mergeSize = $xml->getAttribute('table:number-columns-repeated');
$mergeSize = empty($mergeSize) ? 1 : (int) $mergeSize;
self::checkColumnsRepeatedInt(
$currCells,
$mergeSize
);
$currCells += (int) $mergeSize;
}
if ($doread) {
@@ -380,15 +396,19 @@ class Ods extends BaseReader
break;
case 'table-column':
if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) {
$rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated');
$colRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated');
} else {
$rowRepeats = 1;
$colRepeats = 1;
}
self::checkColumnsRepeatedInt(
$tableColumnIndex - 1,
$colRepeats
);
$tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name');
if (isset($columnWidths[$tableStyleName])) {
$columnWidth = new HelperDimension($columnWidths[$tableStyleName]);
$tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex);
for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) {
for ($colRepeats2 = $colRepeats; $colRepeats2 > 0; --$colRepeats2) {
$spreadsheet->getActiveSheet()
->getColumnDimension($tableColumnString)
->setWidth($columnWidth->toUnit('cm'), 'cm');
@@ -397,7 +417,7 @@ class Ods extends BaseReader
);
}
}
$tableColumnIndex += $rowRepeats;
$tableColumnIndex += $colRepeats;
break;
case 'table-row':
@@ -406,6 +426,7 @@ class Ods extends BaseReader
} else {
$rowRepeats = 1;
}
self::checkRowsRepeated($rowID, $rowRepeats);
$columnID = 'A';
/** @var DOMElement|DOMText $cellData */
@@ -420,6 +441,10 @@ class Ods extends BaseReader
} else {
$colRepeats = 1;
}
self::checkColumnsRepeated(
$columnID,
$colRepeats
);
for ($i = 0; $i < $colRepeats; ++$i) {
StringHelper::stringIncrement(
@@ -594,6 +619,10 @@ class Ods extends BaseReader
} else {
$colRepeats = 1;
}
self::checkColumnsRepeated(
$columnID,
$colRepeats
);
if ($type !== null) {
for ($i = 0; $i < $colRepeats; ++$i) {
@@ -831,4 +860,28 @@ class Ods extends BaseReader
}
}
}
private static function checkRowsRepeated(int $rowID, int $rowRepeats): void
{
if ($rowRepeats < 1 || $rowID + $rowRepeats - 1 > AddressRange::MAX_ROW) {
throw new Exception("Invalid number-rows-repeated $rowRepeats following row $rowID");
}
}
private static function checkColumnsRepeated(string $colID, int $colRepeats): void
{
$colIndex = Coordinate::columnIndexFromString($colID);
if ($colRepeats < 1 || $colIndex + $colRepeats - 1 > AddressRange::MAX_COLUMN_INT) {
throw new Exception("Invalid number-columns-repeated $colRepeats following column $colID");
}
}
private static function checkColumnsRepeatedInt(int $colIndex, int $colRepeats): void
{
// We don't have column string at this point,
// and colIndex is actually 1 less than it should be.
if ($colRepeats < 1 || $colIndex + $colRepeats > AddressRange::MAX_COLUMN_INT) {
throw new Exception("Invalid number-columns-repeated $colRepeats following column index $colIndex");
}
}
}
@@ -56,6 +56,11 @@ class XmlScanner
throw new Reader\Exception('UTF-7 encoding not permitted');
}
if (substr($xml, 0, Reader\Csv::UTF8_BOM_LEN) === Reader\Csv::UTF8_BOM) {
if (preg_match(self::ENCODING_PATTERN, $xml, $matches) === 1) {
if (strtolower($matches[2]) !== 'utf-8') {
throw new Reader\Exception("BOM says UTF-8 but encoding says {$matches[2]}");
}
}
$xml = substr($xml, Reader\Csv::UTF8_BOM_LEN);
}
+1
View File
@@ -136,6 +136,7 @@ class Drawing extends BaseDrawing
if (str_starts_with($path, 'https:') || str_starts_with($path, 'http:')) {
$ctxArray = [
'http' => [
'follow_location' => 0,
'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'header' => [
//'Connection: keep-alive', // unacceptable performance
@@ -17,6 +17,9 @@ class ServerTest extends TestCase
private const REDIRECT = 'http://' . self::SERVER . '/redirect.php';
private const OLDVALUE = 'LOCAL_REDIRECT_SECRET2';
private const NEWVALUE = 'LOCAL_REDIRECT_SECRET3';
private const IMAGE_DIRECT = __DIR__ . '/blue_square.png.xlsx';
private const IMAGE_REDIRECT = __DIR__ . '/blue_square.php.xlsx';
private const IMAGE_START = 'http://' . self::SERVER . '/blue_square';
public static function setUpBeforeClass(): void
{
@@ -77,10 +80,51 @@ class ServerTest extends TestCase
);
}
private static function allowLocalhost(string $path): bool
{
return str_starts_with($path, self::IMAGE_START);
}
public function xtestImageNoRedirect(): void
{
$file = 'zip://' . self::IMAGE_DIRECT . '#xl/drawings/_rels/drawing1.xml.rels';
$contents = (string) file_get_contents($file);
self::assertStringContainsString(
'Target="' . self::IMAGE_START . '.png"',
$contents
);
$reader = new XlsxReader();
$reader->setAllowExternalImages(true)
->setIsWhiteListed(self::allowLocalhost(...));
$spreadsheet = $reader->load(self::IMAGE_DIRECT);
$sheet = $spreadsheet->getActiveSheet();
self::assertCount(1, $sheet->getDrawingCollection());
$spreadsheet->disconnectWorksheets();
}
public function xtestImageRedirect(): void
{
$file = 'zip://' . self::IMAGE_REDIRECT . '#xl/drawings/_rels/drawing1.xml.rels';
$contents = (string) file_get_contents($file);
self::assertStringContainsString(
'Target="' . self::IMAGE_START . '.php"',
$contents
);
$reader = new XlsxReader();
$reader->setAllowExternalImages(true)
->setIsWhiteListed(self::allowLocalhost(...));
$spreadsheet = $reader->load(self::IMAGE_REDIRECT);
$sheet = $spreadsheet->getActiveSheet();
self::assertCount(0, $sheet->getDrawingCollection());
$spreadsheet->disconnectWorksheets();
}
public function testAll(): void
{
$this->xtestServer();
$this->xtestReadFile();
$this->xtestNew();
$this->xtestImageNoRedirect();
$this->xtestImageRedirect();
}
}
@@ -0,0 +1,3 @@
<?php
header('Location: http://localhost:8080/blue_square.png', true, 302);
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Reader\Ods;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
use PhpOffice\PhpSpreadsheet\Reader\Ods;
use PHPUnit\Framework\TestCase;
class BadRepeatsTest extends TestCase
{
public function testBadRepeatedColsRead(): void
{
$this->expectException(ReaderException::class);
$this->expectExceptionMessage('Invalid number-columns-repeated');
$reader = new Ods();
$infile = 'tests/data/Reader/Ods/BadRepeatCol.ods';
$reader->load($infile);
}
public function testBadRepeatedColsWorksheetInfo(): void
{
$this->expectException(ReaderException::class);
$this->expectExceptionMessage('Invalid number-columns-repeated');
$reader = new Ods();
$infile = 'tests/data/Reader/Ods/BadRepeatCol.ods';
$reader->listWorksheetInfo($infile);
}
public function testBadRepeatedRowsRead(): void
{
$this->expectException(ReaderException::class);
$this->expectExceptionMessage('Invalid number-rows-repeated');
$reader = new Ods();
$infile = 'tests/data/Reader/Ods/BadRepeatRow.ods';
$reader->load($infile);
}
public function testBadRepeatedRowsWorksheetInfo(): void
{
$this->expectException(ReaderException::class);
$this->expectExceptionMessage('Invalid number-rows-repeated');
$reader = new Ods();
$infile = 'tests/data/Reader/Ods/BadRepeatRow.ods';
$reader->listWorksheetInfo($infile);
}
}
@@ -73,10 +73,10 @@ class OdsInfoTest extends TestCase
],
[
'worksheetName' => 'Second Sheet',
'lastColumnLetter' => 'A',
'lastColumnIndex' => 0,
'lastColumnLetter' => 'B',
'lastColumnIndex' => 1,
'totalRows' => 2,
'totalColumns' => 1,
'totalColumns' => 2,
],
], $wsinfo);
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
use PHPUnit\Framework\TestCase;
class Sec973cTest extends TestCase
{
public function test937c(): void
{
$this->expectException(ReaderException::class);
$this->expectExceptionMessage('BOM says UTF-8 but encoding says ISO-2022-JP');
$reader = new XlsxReader();
$reader->load('tests/data/Reader/XLSX/sec937c.xlsx');
}
}
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xml;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
use PhpOffice\PhpSpreadsheet\Reader\Xml as XmlReader;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PHPUnit\Framework\TestCase;
class Sec25mgTest extends TestCase
{
private string $filename = '';
private string $xmlns = XmlReader::NAMESPACES_SS;
private string $xmlnsss = XmlReader::NAMESPACES_SS;
protected function tearDown(): void
{
if ($this->filename !== '') {
unlink($this->filename);
}
}
public function testNumericEntities(): void
{
$this->filename = File::temporaryFilename();
$entity_value = str_repeat('A', 100000);
$refs = str_repeat('&big;', 200);
$xml = <<<EOF
<?xml version="1.0"?>
<?mso-application progid="Excel.Sheet"?>
&#38;#60;!DOCTYPE Workbook [
&#38;#60;!ENTITY big "$entity_value">
]>
<Workbook
xmlns="{$this->xmlns}"
xmlns:ss="{$this->xmlnsss}"
>
<Worksheet ss:Name="Sheet1">
<Table><Row><Cell>
<Data ss:Type="String">$refs</Data>
</Cell></Row></Table>
</Worksheet>
</Workbook>
EOF;
self::assertNotFalse(
file_put_contents($this->filename, $xml)
);
$this->expectException(ReaderException::class);
$this->expectExceptionMessage('Cannot load invalid XML file');
$reader = new XmlReader();
$reader->load($this->filename);
}
public function testDetectDoctype(): void
{
$this->filename = File::temporaryFilename();
$entity_value = str_repeat('A', 100000);
$refs = str_repeat('&big;', 200);
$xml = <<<EOF
<?xml version="1.0"?>
<?mso-application progid="Excel.Sheet"?>
<!DOCTYPE Workbook [
&#38;#60;!ENTITY big "$entity_value">
]>
<Workbook
xmlns="{$this->xmlns}"
xmlns:ss="{$this->xmlnsss}"
>
<Worksheet ss:Name="Sheet1">
<Table><Row><Cell>
<Data ss:Type="String">$refs</Data>
</Cell></Row></Table>
</Worksheet>
</Workbook>
EOF;
self::assertNotFalse(
file_put_contents($this->filename, $xml)
);
$this->expectException(ReaderException::class);
$this->expectExceptionMessage('Detected use of ENTITY');
$reader = new XmlReader();
$reader->load($this->filename);
}
public function testNoDoctype(): void
{
$this->filename = File::temporaryFilename();
$refs = str_repeat('&amp;&pi;&#x03cF;&#48;', 200);
$refsOut = str_repeat('&πϏ0', 200);
$xml = <<<EOF
<?xml version="1.0"?>
<?mso-application progid="Excel.Sheet"?>
<Workbook
xmlns="{$this->xmlns}"
xmlns:ss="{$this->xmlnsss}"
>
<Worksheet ss:Name="Sheet1">
<Table><Row><Cell>
<Data ss:Type="String">$refs</Data>
</Cell></Row></Table>
</Worksheet>
</Workbook>
EOF;
self::assertNotFalse(
file_put_contents($this->filename, $xml)
);
// no 'unentity' function in this branch
$this->expectException(ReaderException::class);
$this->expectExceptionMessage('Cannot load invalid XML file');
$reader = new XmlReader();
$spreadsheet = $reader->load($this->filename);
$sheet = $spreadsheet->getActiveSheet();
self::assertSame($refsOut, $sheet->getCell('A1')->getValue());
$spreadsheet->disconnectWorksheets();
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.