{"id":"CVE-2026-108359","aliases":["GHSA-j2w7-387v-m2rq"],"url":"https://o3.security/vulnerability/CVE-2026-108359","summary":"PhpSpreadsheet ODS Reader: Unbounded number-rows-repeated → CPU & Memory Exhaustion (DoS)","details":"## Summary\n\nWhen PhpSpreadsheet loads an OpenDocument Spreadsheet (`.ods`), `Ods::processTableRow()`\nreads the `table:number-rows-repeated` attribute of a `<table:table-row>` with an unchecked \n`(int)` cast, then materialises one cell per repeated row. This value is **completely unbounded**, \nso a **single ~1.4 KB file** can instruct the reader to create up to **millions of cells**, \nexhausting memory (fatal OOM). No authentication, no special configuration, and the default read \nfilter are required - any application that loads a user-supplied `.ods` is affected.\n\n---\n\n## Affected versions\n\nThe `number-rows-repeated` expansion has existed in the ODS reader since its inception\n(pre-dating the current release branches), so all maintained lines are affected.\n\n| Branch | Affected |\n|--------|----------|\n| 1.x    | ≤ 1.30.6 |\n| 2.0–2.1 | ≤ 2.1.18 |\n| 2.2–2.4 | ≤ 2.4.7 |\n| 3.x    | ≤ 3.10.7 |\n| 4.x–5.x | ≤ 5.9.0 (latest) |\n\n---\n\n## Root cause (source → sink)\n\n`src/PhpSpreadsheet/Reader/Ods.php`:\n\n```php\n// line 816 — row repeat, unchecked (int) cast\n$rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated');\n...\n// line 1151 — row materialisation loop: NO upper bound\nfor ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {\n    $rID = $rowID + $rowAdjust;\n    $cell = $spreadsheet->getActiveSheet()->getCell($columnID . $rID);  // allocates a Cell\n    $cell->setValueExplicit($dataValue, $type);\n    // ... no bounds check on $rowAdjust or $rID ...\n}\n```\n\n**Why columns are NOT vulnerable (already bounded):**\n\nThe column-header loop at line 1326 shows the correct pattern with bounds checking:\n\n```php\n// line 1326 (processTableColumnHeader): BOUNDED correctly\nfor ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0 && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT; --$rowRepeats2) {\n    // HAS BOUNDS CHECK: && $tableColumnIndex2 <= AddressRange::MAX_COLUMN_INT\n}\n```\n\nAdditionally, the cell-data column loop at line 1141 calls `getCell()` which throws exception \nat `Coordinate.php:448` when column exceeds `MAX_COLUMN_INT` (XFD / 16,384):\n\n```php\n// Coordinate.php line 448\nif ($columnIndex > self::MAX_COLUMN_INT) {\n    throw new Exception('Column string index can not be beyond XFD');\n}\n```\n\n**Result:** Columns are capped at 16,384. Only rows are unbounded.\n\n---\n\n## Impact\n\n**Memory exhaustion (single valued cell, repeated many rows):**  \nA non-null cell value with a huge `number-rows-repeated` attribute materialises millions of \n`Cell` objects → PHP fatal `Allowed memory size ... exhausted`.\n\n**Example:**\n- Payload: `<table:table-row table:number-rows-repeated=\"5000000\">` (5 million rows)\n- File size: 1.4 KB\n- Memory consumed: ~256 MB (one Cell object per row)\n- Result: OOM at default memory limit\n\n---\n\n## Reproduction\n\n**Vulnerable payload (causes OOM):**\n\n```xml\n<table:table table:name=\"Sheet1\">\n <table:table-row table:number-rows-repeated=\"5000000\">\n  <table:table-cell office:value-type=\"float\" office:value=\"1\">\n   <text:p>1</text:p>\n  </table:table-cell>\n </table:table-row>\n</table:table>\n```\n\n**Not vulnerable (for reference - throws exception):**\n\n```xml\n<table:table table:name=\"Sheet1\">\n <table:table-row>\n  <table:table-cell office:value-type=\"float\" office:value=\"1\"\n      table:number-columns-repeated=\"1000000000\">\n   <text:p>1</text:p>\n  </table:table-cell>\n </table:table-row>\n</table:table>\n```\n\nThis column-repeat example throws `Column string index can not be beyond XFD` exception\n(bounded defense, already in place).","published":"2026-10-22T17:32:49.724Z","modified":"2026-10-28T02:30:26.236717523Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Packagist","name":"phpoffice/phpspreadsheet","fixedVersion":"5.10.0"},{"ecosystem":"Packagist","name":"phpoffice/phpspreadsheet","fixedVersion":"3.10.8"},{"ecosystem":"Packagist","name":"phpoffice/phpspreadsheet","fixedVersion":"2.4.8"},{"ecosystem":"Packagist","name":"phpoffice/phpspreadsheet","fixedVersion":"2.1.19"},{"ecosystem":"Packagist","name":"phpoffice/phpspreadsheet","fixedVersion":"1.30.7"}],"fix":{"url":"https://github.com/PHPOffice/PhpSpreadsheet/commit/299993894dce37aa3b64aa23e9a3f4bb77ab0e66","label":"PHPOffice/PhpSpreadsheet@2999938"},"references":[{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/1.30.7"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.1.19"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.4.8"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/3.10.8"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/5.10.0"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/108xxx/CVE-2026-108359.json"},{"type":"ADVISORY","url":"https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-j2w7-387v-m2rq"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-108359"},{"type":"FIX","url":"https://github.com/PHPOffice/PhpSpreadsheet/commit/299993894dce37aa3b64aa23e9a3f4bb77ab0e66"},{"type":"FIX","url":"https://github.com/PHPOffice/PhpSpreadsheet/commit/2de29384aa11223ee08136c0e6a9e9981bc1a2eb"},{"type":"FIX","url":"https://github.com/PHPOffice/PhpSpreadsheet/commit/980b5c294493856f1a25754f1e8a1d2185b93738"},{"type":"FIX","url":"https://github.com/PHPOffice/PhpSpreadsheet/commit/9a0b48622ebfe93fa48cb9bc09ce57303bb07af9"},{"type":"FIX","url":"https://github.com/PHPOffice/PhpSpreadsheet/commit/da13f40e8760d599b0e8a296017d4b028b20e86c"},{"type":"PACKAGE","url":"https://github.com/PHPOffice/PhpSpreadsheet"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-28T02:30:26.236717523Z"}}