# PHPSpreadsheet has a patch bypass for CVE-2026-34084

PhpSpreadsheet's prohibitWrappers patch for CVE-2026-34084 is bypassed by phar:/// triple-slash paths, restoring phar deserialization and RCE on PHP 7.x.

- Payout: Research
- Severity: critical
- Program: PhpSpreadsheet
- Found by: everping
- Disclosed: 2026-06-08
- CVE: CVE-2026-45034
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-87m4-826x-3crx
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-87m4-826x-3crx
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:critical tech:php technique:incomplete-fix technique:parser-differential technique:source-review vuln:deserialization vuln:path-traversal

**Target:** File::prohibitWrappers / IOFactory::load file path handling
**Root cause:** prohibitWrappers relies on parse_url($filename, PHP_URL_SCHEME) and only rejects when the result is a string longer than 1 char; for phar:///path parse_url returns boolean false, so the check is skipped while PHP's stream layer still resolves the phar wrapper.
**Steps:**
1. Build exploit.phar with phar.readonly=0 containing a gadget object in metadata (build-phar.php).
2. Install phpoffice/phpspreadsheet 1.30.4 on php:7.4-cli.
3. Call IOFactory::load('phar:///work/exploit.phar/dummy.csv').
4. Negative control: phar://x/dummy.csv is rejected by prohibitWrappers.
5. Positive control: phar:///work/exploit.phar/dummy.csv bypasses the check; on PHP 7.4 phar metadata is auto-deserialized, __wakeup/__destruct run, marker file pwned_marker written.
6. On PHP 8.3 with 5.7.0 the bypass still occurs but only yields a phar wrapper file read primitive absent a Phar::getMetadata call.
**How found:** Reporter examined the CVE-2026-34084 fix and noticed prohibitWrappers uses parse_url scheme detection, which is not equivalent to detecting a wrapper; tested slash-count variants and found phar:/// returns false from parse_url while PHP still treats it as a phar wrapper.
**Exploitation:** Attacker supplies a path of form phar:///path/exploit.phar/inner to IOFactory::load. prohibitWrappers returns without throwing because parse_url yields false; PHP's stream layer opens the phar. On PHP 7.x, is_file on the phar triggers automatic metadata deserialization, invoking __wakeup/__destruct of the attacker's gadget object and achieving RCE in the victim process (marker file written). On PHP 8.x only a file read primitive remains unless downstream calls Phar::getMetadata.
**Escalation:** Chains patch bypass into phar metadata deserialization gadget for full RCE on PHP 7.x; downgraded to file read on PHP 8.x.
**Insight:** When auditing wrapper/path validation fixes, test parse_url edge cases: extra slashes after the scheme (phar:///) make PHP_URL_SCHEME return false, so any is_string($scheme) guard silently passes while the stream layer still honors the wrapper. Prefer str_contains($filename, '://') or realpath().

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

**Positive control bypass path phar triple slash**

```
The positive control uses `phar:///work/exploit.phar/dummy.csv` to show that the three slash variant slips through.
```

**Vulnerable prohibitWrappers helper source**

```
public static function prohibitWrappers(string $filename): void
{
    $scheme = parse_url($filename, PHP_URL_SCHEME);
    if (is_string($scheme) && strlen($scheme) > 1) {
        throw new Exception("Stream wrappers are not permitted as file paths: {$filename}");
```

