# PhpSpreadsheet: XmlScanner Bypass via ISO-2022 Escape Sequences Allows XXE and Denial of Service

PhpSpreadsheet's XmlScanner DOCTYPE regex only tolerates NULL bytes, so ISO-2022 ESC sequences interleaved into `<!DOCTYPE` bypass it, enabling entity-expansion DoS and XXE.

- Payout: Research
- Severity: high
- Program: PhpSpreadsheet
- Found by: e1abrador
- Disclosed: 2026-10-27
- CVE: CVE-2026-108358
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-3x94-m842-937c
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-3x94-m842-937c
- Tags: area:oss domain:oss impact:data-leak impact:dos kind:research platform:ghsa sev:high tech:php technique:parser-differential technique:source-review vuln:dos vuln:xxe

**Target:** XLSX reader XML scanner (XmlScanner::scan) before simplexml_load_string
**Root cause:** The DOCTYPE detection regex `\0*<\0*!\0*D...` only allows optional NULL bytes between characters, so ISO-2022 ESC sequences inserted between characters prevent the match while libxml2 strips them during charset normalization and parses a real DOCTYPE.
**Steps:**
1. Build `$esc = "\x1b\x28\x42"` (ISO-2022-JP switch to ASCII) and interleave it between each character of `<!DOCTYPE`.
2. Craft sharedStrings.xml with that DOCTYPE declaring `<!ENTITY big "AAAA...">` (100000 chars) and a cell value of `&big;` repeated 200 times.
3. Package into a minimal XLSX ZIP with [Content_Types].xml, _rels/.rels, xl/workbook.xml, xl/_rels/workbook.xml.rels, xl/worksheets/sheet1.xml, xl/sharedStrings.xml.
4. Load with `new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $reader->load('poc.xlsx')` under `php -d memory_limit=512M`.
5. Observe scanner misses DOCTYPE, entities expand, ~45 MB peak memory from ~2 KB file.
**How found:** Noticed the scanner's regex only accounted for NULL-byte interleaving and hypothesized other charset escape sequences stripped by libxml2 during normalization would break the match while still parsing as DOCTYPE.
**Exploitation:** Attacker uploads a crafted XLSX to any app parsing it with PhpSpreadsheet; internal entity expansion causes quadratic blowup (~22,000:1 amplification) exhausting server memory, and on PHP <8.0 or with LIBXML_NOENT, SYSTEM entities yield local file read and SSRF.
**Escalation:** Multiple concurrent uploads exhaust server memory; XXE escalates to file read/SSRF where libxml2 defaults permit.
**Insight:** When a blacklist regex tolerates one obfuscation (NULL bytes), enumerate every byte sequence the downstream parser silently strips during charset normalization (ISO-2022 ESC designators, BOMs) — the sanitizer and parser disagree on what the input means.

## Proof of concept (copied exactly from the original report)

**DOCTYPE with ESC bytes between characters**

```
$esc = "\x1b\x28\x42";

// Build <!DOCTYPE with ESC bytes between each character
$doctype = "<".$esc."!".$esc."D".$esc."O".$esc."C".$esc."T".$esc."Y".$esc."P".$esc."E";
```

