CVE-2026-34084: Verified Repro With Script Download
CVE-2026-34084: PhpSpreadsheet: SSRF via unsafe stream wrapper in IOFactory::load
CVE-2026-34084 is verified against phpoffice/phpspreadsheet · composer. Affected versions: <= 1.30.2 (also 2.0.0–2.1.14, 2.2.0–2.4.3, 3.3.0–3.10.3, 4.0.0–5.5.0). Vulnerability class: SSRF. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00150.
What Is CVE-2026-34084?
CVE-2026-34084 is a critical SSRF and PHP object-injection vulnerability in PhpSpreadsheet's IOFactory::load(), caused by an unsafe filename validation that lets attacker-controlled paths use PHP stream wrappers. Pruva reproduced it (reproduction REPRO-2026-00150).
CVE-2026-34084 Severity & CVSS Score
CVE-2026-34084 is rated critical severity, with a CVSS base score of 9.8 out of 10.
Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
Affected phpoffice/phpspreadsheet Versions
phpoffice/phpspreadsheet · composer versions <= 1.30.2 (also 2.0.0–2.1.14, 2.2.0–2.4.3, 3.3.0–3.10.3, 4.0.0–5.5.0) are affected.
How to Reproduce CVE-2026-34084
pruva-verify REPRO-2026-00150 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00150/artifacts/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-34084
Reproduced by Pruva's autonomous agents — 178 tool calls over 13 min. Full root-cause analysis and the complete transcript are below.
How the agent worked
Root Cause and Exploit Chain for CVE-2026-34084
Summary
PhpSpreadsheet's IOFactory::load() and IOFactory::identify() methods validate user-supplied filenames through File::assertFile(), which internally relies on PHP's is_file() function. PHP's is_file() returns true for various stream wrapper protocols such as ftp://, phar://, and ssh2.sftp://. Because the library did not explicitly reject stream wrappers, an attacker-controlled filename could trigger an outbound network connection (SSRF via ftp://) or PHAR metadata deserialization (object injection via phar://). The fix adds a prohibitWrappers() guard in File::assertFile() and File::testFileNoThrow() that rejects any path containing a recognized URL scheme before is_file() is evaluated.
Impact
- Package:
phpoffice/phpspreadsheet(Packagist) - Repository: https://github.com/PHPOffice/PhpSpreadsheet
- Affected versions:
<= 1.30.2(and corresponding ranges in 2.x, 3.x, 4.x, 5.x lines) - Fixed versions:
1.30.3,2.1.15,2.4.4,3.10.4,5.6.0 - Severity: Critical (CVSS 9.2)
- Consequences:
- SSRF: Outbound connections to attacker-controlled hosts via
ftp://paths. - PHAR Deserialization: Object injection via
phar://paths, potentially leading to remote code execution.
- SSRF: Outbound connections to attacker-controlled hosts via
Root Cause
In src/PhpSpreadsheet/Shared/File.php, the assertFile() method was implemented as:
public static function assertFile(string $filename, string $zipMember = ''): void
{
if (!is_file($filename)) {
throw new ReaderException('File "' . $filename . '" does not exist.');
}
// ...
}
PHP's is_file() natively supports stream wrappers. When passed an ftp:// path, is_file() initiates an FTP connection to the remote host. When passed a phar:// path, is_file() triggers PHAR metadata deserialization. The library never validated the filename for stream-wrapper schemes before delegating to is_file().
The fix (commit b6c44a4c / tag 1.30.3) adds a prohibitWrappers() helper:
public static function prohibitWrappers(string $filename): void
{
$scheme = parse_url($filename, PHP_URL_SCHEME);
if (is_string($scheme) && strlen($scheme) > 1) {
throw new Exception(
"Stream wrappers are not permitted as file paths: {$filename}"
);
}
}
This is called at the top of both assertFile() and testFileNoThrow(), ensuring that any path with a recognized scheme is rejected before any I/O (including is_file()) occurs.
Reproduction Steps
The reproduction script is repro/reproduction_steps.sh. It performs the following steps:
- Checks for
php,composer, andpython3availability. - Finds an ephemeral TCP port on
127.0.0.1. - In a scratch directory, installs
phpoffice/phpspreadsheet:1.30.2(vulnerable). - Creates a PHP script that calls
IOFactory::load("ftp://127.0.0.1:<port>/x"). - Starts a Python TCP listener on the chosen port.
- Runs the PHP script and records whether the listener receives a connection.
- Repeats steps 3–6 for
phpoffice/phpspreadsheet:1.30.3(fixed). - Compares results and writes
validation_verdict.json.
Expected evidence:
- Vulnerable (1.30.2): The Python listener logs
CONNECTION_RECEIVEDbecauseis_file("ftp://...")opens an outbound FTP connection before failing. - Fixed (1.30.3): The Python listener logs
NO_CONNECTIONbecauseprohibitWrappers()throws an exception beforeis_file()is reached.
Evidence
Log files are written to $ROOT/logs/ during script execution:
logs/vuln_listener.log:CONNECTION_RECEIVED from ('127.0.0.1', <port>)logs/vuln_php.log:EXCEPTION: File "ftp://127.0.0.1:<port>/x" does not exist.logs/fixed_listener.log:NO_CONNECTIONlogs/fixed_php.log:EXCEPTION: Stream wrappers are not permitted as file paths: ftp://127.0.0.1:<port>/x
Validation verdict is written to validation_verdict.json:
{
"verdict": "confirmed",
"vulnerable_version": "1.30.2",
"fixed_version": "1.30.3",
"vulnerable_indicator": "CONNECTION_RECEIVED from ('127.0.0.1', ...)",
"fixed_indicator": "NO_CONNECTION",
"vulnerability_type": "SSRF via unsafe stream wrapper in IOFactory::load()",
"details": "PhpSpreadsheet's File::assertFile() used is_file() which accepts PHP stream wrappers like ftp://. Fixed version adds prohibitWrappers() to reject stream wrappers before is_file() is called."
}
Environment:
- PHP 8.4.19
- Composer 2.8.12
- Python 3.x
Recommendations / Next Steps
- Upgrade immediately to
phpoffice/phpspreadsheet >= 1.30.3(or the matching fixed version in your major line). - Input validation: If wrapping
IOFactory::load()in application code, add an independent allow-list or deny-list for filenames before passing them to the library. - Regression test: Add a unit test that asserts
File::assertFile("ftp://...")andFile::assertFile("phar://...")throw exceptions. - Disable unnecessary wrappers: In hardened deployments, consider disabling the
ftpandpharPHP stream wrappers viastream_wrapper_unregister()if not required by the application.
Additional Notes
- Idempotency: The reproduction script has been executed twice consecutively with identical results, confirming reproducibility.
- SSRF vector chosen: The SSRF vector (
ftp://) was selected over PHAR deserialization because it produces a clean, deterministic, network-free observable on localhost (a TCP connection attempt). No external network, database, or browser is required. - Edge cases: The fix uses
strlen($scheme) > 1to avoid false positives on Windows absolute paths (e.g.,C:\...), since single-character schemes do not correspond to any known PHP stream wrapper.
CVE-2026-34084 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-34084
Scripts, logs, diffs, and output captured during the reproduction.
No artifacts available
How to Fix CVE-2026-34084
FAQ: CVE-2026-34084
How does the PhpSpreadsheet stream-wrapper exploit work?
Which PhpSpreadsheet versions are affected by CVE-2026-34084, and where is it fixed?
How severe is CVE-2026-34084?
How can I reproduce CVE-2026-34084?
References for CVE-2026-34084
Authoritative sources for CVE-2026-34084 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.