PhpSpreadsheet has CPU Denial of Service via Unbounded Row Index in SpreadsheetML XML Reader
CVE-2026-40863 · GHSA-84wq-86v6-x5j6 · published 29 Apr 2026
How serious is it
- Severity: high, CVSS score 7.5 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-40863?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
phpoffice/phpspreadsheet(Packagist) >= 4.0.0, <= 5.6.0phpoffice/phpspreadsheet(Packagist) >= 3.3.0, <= 3.10.4phpoffice/phpspreadsheet(Packagist) >= 2.2.0, <= 2.4.4phpoffice/phpspreadsheet(Packagist) >= 2.0.0, <= 2.1.15phpoffice/phpspreadsheet(Packagist) <= 1.30.5
How to check
- Run
composer show phpoffice/phpspreadsheetin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- On >= 4.0.0, <= 5.6.0: Upgrade
phpoffice/phpspreadsheetto 5.7.0 or later, commit the lockfile and redeploy. - On >= 3.3.0, <= 3.10.4: Upgrade
phpoffice/phpspreadsheetto 3.10.5 or later, commit the lockfile and redeploy. - On >= 2.2.0, <= 2.4.4: Upgrade
phpoffice/phpspreadsheetto 2.4.5 or later, commit the lockfile and redeploy. - On >= 2.0.0, <= 2.1.15: Upgrade
phpoffice/phpspreadsheetto 2.1.16 or later, commit the lockfile and redeploy. - On <= 1.30.5: Upgrade
phpoffice/phpspreadsheetto 1.30.6 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## Summary
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:
```php
// 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()`:
```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 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`:
```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:
```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.
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-84wq-86v6-x5j6
- nvd.nist.gov/vuln/detail/CVE-2026-40863
- github.com/advisories/GHSA-84wq-86v6-x5j6
- github.com/PHPOffice/PhpSpreadsheet
Merged from GitHub Advisory Database, OSV. Last updated 27 Oct 2026. All vulnerabilities · RSS · JSON