Skip to main content
← All articles
php

The phar wrapper that wouldn't die: parse_url() ate PhpSpreadsheet's patch

The phar wrapper that wouldn't die: parse_url() ate PhpSpreadsheet's patch

A patch bypass is a special kind of embarrassing, because the bug was already "fixed." PhpSpreadsheet had a phar-wrapper vulnerability, shipped a guard against it, and then someone found that adding one extra slash walks straight past the guard. The root cause isn't really PhpSpreadsheet's, though. It's parse_url() — a function the whole PHP ecosystem keeps mistaking for a security boundary, and which was never built to be one.

The vulnerability

CVE-2026-45034 is, per its own advisory title, the "CVE-2026-34084 patch bypass." The earlier fix added a helper, File::prohibitWrappers(), meant to reject dangerous stream wrappers like phar:// in filenames. It works by calling parse_url($filename, PHP_URL_SCHEME) and checking that the result is a string longer than one character. Reasonable-looking. The problem: feed it a filename with three or more slashes after the scheme — phar:///path/to/exploit.phar/dummy.csv — and parse_url() returns boolean false instead of the scheme. The is_string() check is now false, the rejection branch is skipped, the guard waves it through — and PHP's stream layer, which doesn't care about parse_url()'s opinion, happily treats it as a valid phar wrapper and opens it.

One quick correction to the numbers going around: the CVSS is 9.2 (CVSS 4.0) per the GitHub advisory and NVD, not the 9.8 you'll see in some write-ups (that figure is one vendor's CVSS 3.1-style estimate). It affects PhpSpreadsheet 1.30.4 and earlier; upgrade to 1.30.5. On PHP 7 it's full remote code execution; on PHP 8, where automatic phar metadata deserialization was removed, it degrades to a file-read primitive unless downstream code calls Phar::getMetadata(), at which point RCE is back on the menu.

A one-minute refresher on why phar is scary

A phar archive stores serialized metadata in its manifest. When any filesystem function touches a phar:// path — file_exists, is_file, getimagesize, anything — older PHP would deserialize that metadata automatically, instantiating attacker-controlled objects and firing their magic methods (__wakeup, __destruct). No explicit unserialize() call required. Sam Thomas demonstrated this at Black Hat in 2018, PHP 8.0 removed the automatic behavior for basic file ops, libraries added guards, and everyone declared the class dead. It is not dead. PHP 7 is still everywhere, the guards are bypassable (exhibit A, above), and the gadget chains still ship in common dependencies.

The actual villain: parse_url() as a gatekeeper

Here's the part worth internalizing, because it's bigger than one library. parse_url() is lenient and not strictly spec-compliant — its return value depends on slash counts and on the PHP version. It exists to split a URL you already trust into parts, not to decide whether an untrusted string is dangerous. Every time someone builds an allow/deny check on top of it, they're trusting a function whose edge cases were never meant to carry security weight. This is the same family of mistake I've hit elsewhere — treating a lenient parser's "good enough for display" output as if it were a hard yes/no gate.

The bug wasn't that a slash count fooled a guard. The bug was building the guard on a function that splits URLs for convenience and treating its quirks as a security contract.

The most credible voice on this is PhpSpreadsheet's own remediation note, which tells developers not to use parse_url() for wrapper detection precisely because "its behavior depends on the slash count and on the PHP version," and to check for the :// substring or use realpath() instead (realpath() returns false for wrapper-prefixed paths — a much harder thing to trick). When the maintainer of the affected library tells you the function is unfit for the job, believe them.

What to take away

Upgrade PhpSpreadsheet to 1.30.5 if you parse spreadsheets anywhere near untrusted input — and remember that "we render user-uploaded XLSX" is a bigger attack surface than it feels like. But the durable lesson is the one that generalizes past this CVE: when you're deciding whether an untrusted string is safe, don't reach for the convenience parser. parse_url(), and functions like it, answer "how do I split this?" — not "should I be afraid of this?" Those are different questions, and a decade of phar bugs is what it looks like to keep confusing them.

Links

BM
Blue Moose
The moose behind Blue Moose. Full-stack PHP developer — Drupal by day, Symfony by night, tests always.