CVE-2026-34084 is a critical (CVSS 9.8) flaw in PhpSpreadsheet, the PHP spreadsheet library. If user input reaches the filename passed to IOFactory::load, an attacker can supply a stream wrapper path. Fixed versions are 1.30.3, 2.1.15, 2.4.4, 3.10.4 and 5.6.0. Check where your code takes filenames.
What's affected
The project's advisory (GHSA-q4q6-r8wh-5cgh) lists these ranges for the Composer package phpoffice/phpspreadsheet, with the matching fixed release for each:
- 1.30.2 and earlier: fixed in 1.30.3
- 2.0.0 through 2.1.14: fixed in 2.1.15
- 2.2.0 through 2.4.3: fixed in 2.4.4
- 3.3.0 through 3.10.3: fixed in 3.10.4
- 4.0.0 through 5.5.0: fixed in 5.6.0
The trigger condition matters. The flaw applies when the filename argument to IOFactory::load() is user-controlled. An application that only opens files from fixed, trusted paths is not exposed through this route, though upgrading is still wise.
The record describes two outcomes. The phar:// wrapper triggers deserialization of PHAR metadata, which can lead to remote code execution if the application contains a suitable gadget chain. The ftp:// and ssh2.sftp:// wrappers can be used for server-side request forgery.
How to check if you're exposed
List the installed version in each PHP project:
composer show phpoffice/phpspreadsheet
composer why phpoffice/phpspreadsheet
The second command shows which of your packages pulls it in, which matters because many reporting, export and CMS plugins bring PhpSpreadsheet along without saying so.
Compare the version with the list above. Then search your own code for calls to IOFactory::load and the reader classes, and trace where the filename comes from. If any part of it traces back to a request parameter, a form field or a stored value that a user can edit, you are in the exposed case.
Also check composer.lock files in every repository and deployed image. A scan of the lockfile is the quickest way to cover many projects at once. Paste a lockfile or manifest into the free in-browser dependency scan to see whether the package is in your tree, including as a transitive dependency. Reachability analysis then helps you judge whether your code calls the affected path, so you can order the work sensibly.
Use Composer's built-in audit to see known advisories for everything in a project:
composer audit
It reports advisories for installed packages, which helps when PhpSpreadsheet arrives through a plugin you never chose. Run it in CI so new advisories show up on the next build.
When you search your code, look at the full path from request to loader. A filename can pass through helper functions, a database column or a queue before it reaches IOFactory::load. If you cannot trace it with confidence, treat it as user-controlled.
What to do now
- List every project that depends on PhpSpreadsheet, directly or through another package.
- Update to the fixed release on your branch (1.30.3, 2.1.15, 2.4.4, 3.10.4 or 5.6.0), then run your tests.
- Search for
IOFactory::loadand reader calls whose filename can come from user input. - Never pass a raw user value as a path. Accept an upload, store it under a name you generate, and load that path.
- Reject any path that contains a scheme such as
phar://,ftp://orssh2.sftp://before it reaches the library. - Review outbound network rules from the app servers, so a request forgery has fewer places to go.
- Run
composer auditin CI so advisories on dependencies fail a build before they ship.
Why it matters
The record lists CWE-502 (deserialization of untrusted data) and CWE-918 (server-side request forgery). The advisory explains the root cause: the library checks that the filename is a real file with is_file(), inside File::assertFile(). In PHP, that check understands stream wrappers for any wrapper that supports stat(), and phar://, ftp:// and ssh2.sftp:// all do. So a value that looks like a URL passes a check that was meant to confirm a local file.
The PHAR case is the dangerous one. Reading a PHAR archive deserializes its metadata, and deserialization can run code if the application, or any library it loads, contains classes that can be chained together. That chain is not part of this library, so the real impact depends on what else is installed. It is still a good reason not to count on its absence.
The record does not mark this CVE as exploited, and it is not on the CISA KEV list. The fix is small, and the hardening step of not trusting user-supplied paths protects against the same class of mistake in other libraries.
Here is a defensive pattern for code that must accept a path. Resolve it, confirm it lives where you expect and refuse anything that carries a scheme:
$real = realpath($candidate);
if ($real === false || str_contains($candidate, '://') || !str_starts_with($real, $uploadDir)) {
throw new InvalidArgumentException('Unexpected file path');
}
$spreadsheet = IOFactory::load($real);
realpath() returns false for wrapper URLs, so the check refuses them, and the prefix test keeps loads inside your upload directory. This does not replace the upgrade. It shows why upstream's fix and your own validation are both worth having.
Finally, think about the gadget chain caveat in the advisory. The PHAR route to code execution needs classes in your application or its dependencies that do something harmful when deserialized. Large PHP applications often contain such classes. You cannot easily prove that yours does not, so treat the remote code execution outcome as possible and fix it as such.
Try it yourself
Click through it at your own pace, or open it full screen.
Quick check
Does this affect me if users only upload files?
Only if user input reaches the filename passed to the loader. If you save uploads under a name you generate and load that, the trigger is not present, but update anyway.
Sources
Last reviewed: 19 October 2026.