CVE-2026-108359

Updated on 22 Oct 2026

Severity

8.7 High severity

Details

CVSS score
8.7
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Overview

About vulnerability

Summary

When PhpSpreadsheet loads an OpenDocument Spreadsheet (.ods), Ods::processTableRow() reads the table:number-rows-repeated attribute of a <table:table-row> with an unchecked (int) cast, then materialises one cell per repeated row. This value is completely unbounded, so a single ~1.4 KB file can instruct the reader to create up to millions of cells, exhausting memory (fatal OOM). No authentication, no special configuration, and the default read filter are required - any application that loads a user-supplied .ods is affected.


Affected versions

The number-rows-repeated expansion has existed in the ODS reader since its inception (pre-dating the current release branches), so all maintained lines are affected.

Branch Affected
1.x ≤ 1.30.6
2.0–2.1 ≤ 2.1.18
2.2–2.4 ≤ 2.4.7
3.x ≤ 3.10.7
4.x–5.x ≤ 5.9.0 (latest)

Root cause (source → sink)

src/PhpSpreadsheet/Reader/Ods.php:

// line 816 — row repeat, unchecked (int) cast
$rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated');
...
// line 1151 — row materialisation loop: NO upper bound
for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
$rID = $rowID + $rowAdjust;
$cell = $spreadsheet->getActiveSheet()->getCell($columnID . $rID);  // allocates a Cell
$cell->setValueExplicit($dataValue, $type);
// ... no bounds check on $rowAdjust or $rID ...
}

Why columns are NOT vulnerable (already bounded):

The column-header loop at line 1326 shows the correct pattern with bounds checking:

// line 1326 (processTableColumnHeader): BOUNDED correctly
for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$rowRepeats2) {
// HAS BOUNDS CHECK: && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT
}

Additionally, the cell-data column loop at line 1141 calls getCell() which throws exception at Coordinate.php:448 when column exceeds MAX_COLUMN_INT (XFD / 16,384):

// Coordinate.php line 448
if ($columnIndex > self::MAX_COLUMN_INT) {
throw new Exception('Column string index can not be beyond XFD');
}

Result: Columns are capped at 16,384. Only rows are unbounded.


Impact

Memory exhaustion (single valued cell, repeated many rows): A non-null cell value with a huge number-rows-repeated attribute materialises millions of Cell objects → PHP fatal Allowed memory size ... exhausted.

Example:

  • Payload: <table:table-row table:number-rows-repeated="5000000"> (5 million rows)
  • File size: 1.4 KB
  • Memory consumed: ~256 MB (one Cell object per row)
  • Result: OOM at default memory limit

Reproduction

Vulnerable payload (causes OOM):

<table:table table:name="Sheet1">
<table:table-row table:number-rows-repeated="5000000">
<table:table-cell office:value-type="float" office:value="1">
<text:p>1</text:p>
</table:table-cell>
</table:table-row>
</table:table>

Not vulnerable (for reference - throws exception):

<table:table table:name="Sheet1">
<table:table-row>
<table:table-cell office:value-type="float" office:value="1"
table:number-columns-repeated="1000000000">
<text:p>1</text:p>
</table:table-cell>
</table:table-row>
</table:table>

This column-repeat example throws Column string index can not be beyond XFD exception (bounded defense, already in place).

Details

Affected product:
phpoffice/phpspreadsheet
Affected packages:
phpspreadsheet @ 4.5.0 (+1 more)

Fixes