Retour au blog
CVE-2026-34084PhpSpreadsheetPHPPHAR deserializationRCESSRFCVE

PhpSpreadsheet CVE-2026-34084: RCE via phar:// in IOFactory::load()

PhpSpreadsheet (all branches < 5.6.0) allows RCE and SSRF via phar://, ftp:// and ssh2.sftp:// in IOFactory::load(). Patch and PHP hardening guide.

11 mai 20265 min de lecture

PhpSpreadsheet is one of the most-deployed PHP libraries in the world: used by millions of sites to import/export Excel, OpenDocument, and CSV. CVE-2026-34084 (CVSS 9.8) reveals that the IOFactory::load() function accepts PHP wrapper paths (phar://, ftp://, ssh2.sftp://) that pass the File::assertFile() check. The phar:// wrapper triggers PHAR metadata deserialization — a classic path to PHP RCE. The ftp:// and ssh2.sftp:// wrappers open the door to server-side SSRF.

For any site that passes user-controlled data (file path, import URL, upload parameter) to IOFactory::load(), this is an open highway to full compromise.


Technical Details

Vulnerable component

The IOFactory::load(string $filename, ...) method is the standard entry point for loading a spreadsheet file. It first calls File::assertFile($filename) to verify the path is valid. This check uses is_file()which returns true for PHP wrapper paths like phar://, ftp://, ssh2.sftp://.

Once the check passes, PhpSpreadsheet opens the file via standard PHP functions. If the path is phar://archive.phar, PHP automatically deserializes the PHAR archive metadata, which can invoke magic methods (__wakeup, __destruct) on objects whose class is defined in the application code.

This is the classic PHP gadget chain pattern: if the application contains a class with an exploitable magic method (e.g., eval($this->code) in __destruct), the attacker gains arbitrary code execution.

Characteristics

FieldValue
CVSS 3.19.8 (CRITICAL)
VectorAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWECWE-502 (Deserialization of Untrusted Data) + CWE-918 (SSRF)
PrerequisiteIOFactory::load()'s $filename parameter influenced by user input
Prerequisite #2 for RCEPresence of a gadget chain in the code (often provided by Laravel/Symfony/Yii/CodeIgniter frameworks and their dependencies)

Affected Products and Versions

BranchAffected versionsPatched version
1.x≤ 1.30.21.30.3
2.0.x2.0.0 → 2.1.142.1.15
2.2.x → 2.4.x2.2.0 → 2.4.32.4.4
3.3.x → 3.10.x3.3.0 → 3.10.33.10.4
4.x → 5.x4.0.0 → 5.5.05.6.0

Check your version:

composer show phpoffice/phpspreadsheet | grep versions

Exploitation and Impact

Attack scenarios

Scenario 1 — User-supplied Excel import

An application lets users upload an Excel file for processing. The controller receives the stored path and calls IOFactory::load($uploadedFile). The attacker uploads a legitimate-looking file but named evil.phar (or via path traversal forces the phar:// wrapper).

Scenario 2 — Controlled URL parameter

The application accepts a ?file=... parameter to generate a report from a template. The attacker supplies ?file=phar:///tmp/uploaded.phar and triggers deserialization.

Scenario 3 — SSRF via ftp/ssh2.sftp

Even without a working RCE gadget chain, an attacker can use ftp://internal-service:21/file.xlsx to make the PHP server issue requests to internal services unreachable from the internet — typical for attacking a Redis instance, a cloud metadata service (AWS IMDS), or an internal dashboard.

PoC concept (RCE via phar)

// Attacker side — craft the PHAR with gadget chain
$phar = new Phar('evil.phar');
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($gadgetChainObject);  // target's gadget
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
// evil.phar is then uploaded to the target
// Target side (vulnerable)
$file = $_GET['file'];  // attacker controlled
\PhpOffice\PhpSpreadsheet\IOFactory::load($file);
// if $file = 'phar:///tmp/evil.phar', the gadget chain runs

Impact

  • Full RCE in the PHP process (often www-data or the application account)
  • SSRF toward unexposed internal services
  • File read via phar:// pointing at arbitrary paths
  • LAN pivot from the web server

Detection and IOCs

Web server logs

Search access logs for parameters containing phar://, ftp://, ssh2.sftp://:

grep -E "phar://|ftp://|ssh2\.sftp://" /var/log/apache2/access.log

PHP logs

Enable error logging and hunt for:

  • Deserialization warnings
  • PharException errors
  • Fatal errors in __wakeup, __destruct
grep -E "Phar|unserialize|__wakeup|__destruct" /var/log/php/error.log

Application code audit

Locate every call to IOFactory::load():

grep -rn "IOFactory::load" /var/www/your-app/

For each call, verify:

  • Is $filename derived from user input (POST, GET, COOKIE, header)?
  • Is there explicit prefix validation (reject phar://, etc.)?

WAF rule (ModSecurity)

SecRule ARGS "@rx (phar|ftp|ssh2\.sftp)://" \
  "id:'2026034084',phase:2,deny,status:403,\
   msg:'PhpSpreadsheet CVE-2026-34084 wrapper injection attempt',\
   t:lowercase"

Mitigation and Patch

Immediate action: upgrade

# Composer
composer require phpoffice/phpspreadsheet:^5.6.0
# Or for older branches:
composer require phpoffice/phpspreadsheet:^3.10.4
composer require phpoffice/phpspreadsheet:^2.4.4
composer require phpoffice/phpspreadsheet:^2.1.15
composer require phpoffice/phpspreadsheet:^1.30.3

Workaround if upgrading is delayed

  1. Application-side validation before every IOFactory::load() call:
function isSafeSpreadsheetPath(string $path): bool {
    // Reject all PHP wrappers
    $forbidden = ['phar://', 'ftp://', 'ssh2.sftp://', 'http://', 'https://',
                  'data://', 'php://', 'zip://', 'compress.zlib://'];
    foreach ($forbidden as $prefix) {
        if (stripos($path, $prefix) === 0) return false;
    }
    // Force an absolute path within a trusted directory
    $real = realpath($path);
    return $real !== false && str_starts_with($real, '/var/uploads/');
}

if (!isSafeSpreadsheetPath($filename)) {
    throw new InvalidArgumentException("Unsafe path");
}
\PhpOffice\PhpSpreadsheet\IOFactory::load($filename);
  1. Disable the PHAR extension in php.ini if your application doesn't use it:
disable_functions = phar_loader
; or remove the phar extension from loading
  1. WAF with PHP-wrapper input filtering

Long-term hardening

  • Audit every PHP library that calls is_file(), file_exists(), file_get_contents() on user-controlled paths (PhpSpreadsheet isn't the only one with this pattern)
  • Block phar://, ftp://, etc. in parameters at the Apache/Nginx layer
  • Rotate application credentials if you suspect past exploitation
  • Audit PHP logs for old deserialization warnings

Why Continuous Monitoring of Application Dependencies Matters

PhpSpreadsheet is one of those dependencies you install once and forget — yet it ships in thousands of PHP projects, including CMSes, internal tools, and WordPress plugins. A CVE like CVE-2026-34084 instantly affects all those deployments, but few IT departments have visibility into their third-party dependencies.

With cveo.tech, inventory your PHP applications and their key dependency versions and get automatic alerts the moment a critical CVE targets one of your exact versions — including buried components in the composer.lock that nobody looks at anymore.

Surveillez les CVE avec l'IA

Recherche IA, scoring CVSS, surveillance de parc et alertes automatiques.