GitHub Advisoryadvisory · Packagist
GHSA-7c6m-4442-2x6m PhpSpreadsheet has CPU Denial of Service via Unbounded Row Number in XLSX Row Dimensions
HIGH
Official page ↗
- Aliases
- CVE-2026-40902
- Ecosystem
- Packagist
- Packages
- phpoffice/phpspreadsheet
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-400, CWE-770
- Reviewed
- 2026-04-29
Affected packages
• Packagist phpoffice/phpspreadsheet: >= 4.0.0, < 5.7.0 → fixed in 5.7.0
• Packagist phpoffice/phpspreadsheet: >= 3.3.0, < 3.10.5 → fixed in 3.10.5
• Packagist phpoffice/phpspreadsheet: >= 2.2.0, < 2.4.5 → fixed in 2.4.5
• Packagist phpoffice/phpspreadsheet: >= 2.0.0, < 2.1.16 → fixed in 2.1.16
• Packagist phpoffice/phpspreadsheet: < 1.30.4 → fixed in 1.30.4
Description
Summary
The XLSX reader's `ColumnAndRowAttributes::readRowAttributes()` method reads row numbers from XML attributes without validating them against the spreadsheet maximum row limit (`AddressRange::MAX_ROW = 1,048,576`). An attacker can craft a minimal XLSX file (~1.6KB) containing a `` 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::MAX_ROW
```
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::MAX_COLUMN_INT`), and cell coordinates are validated in `Coordinate::coordinateFromString()` (line 40) against `MAX_ROW`. 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
content_types = ''
rels = ''
workbook = ''
wb_rels = ''
sheet = '1'
with zipfile.ZipFile('dos_row.xlsx', 'w', zipfile.ZIP_DEFLATED) as zf:
zf.writestr('[Content_Types].xml', content_types)
zf.writestr('_rels/.rels', rels)
zf.writestr('xl/workbook.xml', workbook)
zf.writestr('xl/_rels/workbook.xml.rels', wb_rels)
zf.writestr('xl/worksheets/sheet1.xml', sheet)
print("Created dos_row.xlsx")
```
Step 2: Load with PhpSpreadsheet (CPU exhaustion)
```php
load('dos_row.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 AddressRange::MAX_ROW) {
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.
Related
Alias · CVE (1)
- CVE-2026-40902 PhpSpreadsheet is a pure PHP library for reading and writing spreadsheet files. vulnerability