A critical PhpSpreadsheet bug walked straight through its own patch

A critical PhpSpreadsheet bug walked straight through its own patch

A few weeks ago the PHPOffice team shipped a fix for a phar deserialization bug in PhpSpreadsheet, tracked as CVE-2026-34084. Researchers then found a way straight around that fix, and the bypass is now its own vulnerability: CVE-2026-45034, rated 9.2 critical on CVSS v4. The advisory, GHSA-87m4-826x-3crx, went public on June 7. If PhpSpreadsheet reads spreadsheet files anywhere in your stack, this one is worth ten minutes today.

The original patch added a helper called File::prohibitWrappers that was meant to reject stream wrappers like phar://. It did that by calling parse_url($filename, PHP_URL_SCHEME) and throwing if the scheme came back as a string longer than one character. The trouble is that this check is not the same question as "does this path use a wrapper." Hand it a path shaped like phar:///path/file.phar/inner, with three slashes after the scheme, and parse_url returns boolean false instead of the scheme string. The check is skipped, the helper returns without complaint, and IOFactory::load() reaches the phar wrapper anyway.

Why the PHP version matters more than the CVSS score

What happens next depends entirely on your PHP version, and this is the part teams read wrong. On PHP 7.x, simply touching the phar wrapper through is_file is enough for PHP to deserialize the phar's metadata, which fires __wakeup and __destruct on attacker-controlled objects. That is full remote code execution. The researchers proved it on the 1.x branch under PHP 7.4 with a gadget chain that wrote a marker file. On PHP 8.x, automatic metadata deserialization for plain file operations was removed, so at the PhpSpreadsheet layer the bug drops to a phar file-read primitive. Code execution only comes back if something downstream in your app calls Phar::getMetadata on that path.

So read it this way. Still on PHP 7.x? This is critical and immediate. On PHP 8.x you are not automatically clear, you are one Phar::getMetadata call away from the same result, and an arbitrary phar file-read is not a bug you want sitting in a document-import endpoint either.

What this says about how patches get bypassed

During our pentesting engagements, phar deserialization is one of the first things we reach for when an app takes a file path, a filename, or an upload and feeds it to a library. The interesting part of this CVE is not the wrapper trick, it is the shape of the original fix. The patch validated the scheme parse_url happened to return, when the real rule was "this path must not reference a wrapper at all." Patch the symptom you can see, and you leave the cases you did not test wide open. Three slashes was all it took.

We see the same reflex outside PHP. From our PHP and Docker work building cloud-native apps for enterprise clients, the libraries that parse untrusted input are the ones that earn a second look on every dependency bump, not just when a CVE lands. In our native iOS and Android projects the pattern repeats with third-party SDKs that quietly accept a URL or a path. The team trusts the library, the library trusts the input, and nobody owns the boundary in between. A new patch is not the end of a vulnerability class, it is one more data point about where the boundary actually is.

What to do this week

Three concrete steps, in order:

  1. Upgrade PhpSpreadsheet. The fix is 1.30.5 on the 1.x branch, or the patched release on whatever branch you track: 2.1.17, 2.4.6, 3.10.6, or 5.8.0. The new code drops the parse_url trick and matches phar:// and similar wrappers with explicit patterns instead.
  2. Grep your codebase for IOFactory::load and Reader::load, then trace each call back to where the path comes from. Any one reachable from a user-supplied filename, upload name, or URL is the one that matters. Pin the patched version before that endpoint ships again.
  3. Add defense in depth at the PHP layer. Disable the phar://, php://, data://, and expect:// stream wrappers where your import code runs, and validate or canonicalize any path before it reaches a loader. If you are still on PHP 7.x, this CVE is one more reason to finish that migration. The runtime itself removed the automatic deserialization sink in PHP 8.

If you build on PHP, a useful habit is to treat every file-handling library as hostile to untrusted paths by default, the way we do when we build and review backend services for clients with real compliance requirements. The cost of that habit is a few minutes per dependency. The cost of skipping it is an RCE in your document pipeline.

If a third-party library quietly reaching for phar:// in your PHP stack sounds like a problem you would rather catch before an attacker does, let's talk.

expert-analysisphpsecuritytech-newsvulnerability