GHSA-xh5m-36r6-47m3 PHPSpreadsheet: XLS/OLE sector-chain self-loop causes memory exhaustion
SummaryPhpSpreadsheet'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.This 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.Vulnerability details`OLERead::read()` loads the input and builds sector chains from attacker-controlled OLE header and allocation-table values:`src/PhpSpreadsheet/Shared/OLERead.php:82` reads the entire file after validating only the OLE magic.`src/PhpSpreadsheet/Shared/OLERead.php:84-97` reads sector-chain metadata from the file header.`src/PhpSpreadsheet/Shared/OLERead.php:132-146` builds `bigBlockChain` and then follows the small-block depot chain.The vulnerable loop is:There 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.The issue is reachable during normal reader detection/loading:`src/PhpSpreadsheet/Reader/XlsBase.php:153-165` calls `OLERead::read()` from `canRead()`.`src/PhpSpreadsheet/Reader/Xls.php:376-383` calls `OLERead::read()` from `loadOLE()`.`src/PhpSpreadsheet/IOFactory.php:181-213` calls `canRead()` while creating a reader for a file, so automatic format detection can trigger the issue.Similar unbounded sector-chain walks exist later in stream reading:`src/PhpSpreadsheet/Shared/OLERead.php:175-180``src/PhpSpreadsheet/Shared/OLERead.php:198-202``src/PhpSpreadsheet/Shared/OLERead.php:218-222`The proof of concept below confirms the small-block depot chain loop; the same remediation pattern should be applied to all sector-chain walks.ImpactA 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.The issue occurs before the file is recognized as a valid workbook stream, so even detection/probing paths are affected.Safe local proof of conceptThis proof of concept uses only Docker with `--network none`; it creates the malformed OLE file inside the container and does not contact external infrastructure.Observed output:Suggested remediationValidate every OLE sector-chain walk with:a visited-sector set to reject cycles;maximum chain length based on file size and sector size;bounds checks before reading from `$this->data`, `$this->bigBlockChain`, or `$this->smallBlockChain`;rejection of negative sector IDs other than the documented end-of-chain marker.Replace fatal memory exhaustion with a recoverable `Reader\Exception` for malformed OLE chains.Apply the same guarded chain-walk helper to:small-block depot chain construction;small-block stream extraction;big-block stream extraction;`readData()`.Add regression tests with self-looping and out-of-range SAT/SSAT chains.
CVSS v3.1 score: 7.5