{
  "ticket_id": "CVE-2026-34084",
  "code_root": "external/phpspreadsheet",
  "source": {
    "type": "cve",
    "cve_id": "CVE-2026-34084",
    "advisory_id": "GHSA-q4q6-r8wh-5cgh",
    "advisory_url": "https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-q4q6-r8wh-5cgh",
    "vendor": "PHPOffice",
    "product": "PhpSpreadsheet",
    "repo": "https://github.com/PHPOffice/PhpSpreadsheet"
  },
  "facts": {
    "cve_id": "CVE-2026-34084",
    "advisory_id": "GHSA-q4q6-r8wh-5cgh",
    "issue_summary": "When the filename passed to PhpSpreadsheet's IOFactory::load() is user-controlled, the File::assertFile() validation relies on PHP's is_file(), which returns true for PHP stream wrappers such as phar://, ftp://, and ssh2.sftp://. Because the disallowed wrapper is not rejected, an ftp:// path causes the library to open an outbound connection (SSRF) and a phar:// path triggers PHAR archive metadata deserialization, allowing object injection.",
    "vulnerability_type": "Unsafe stream-wrapper handling -> SSRF and PHAR deserialization",
    "suspected_cwe": ["CWE-502", "CWE-918"],
    "affected_versions": "<= 1.30.2 (also affected ranges in the 2.x, 3.x, 4.x and 5.x lines)",
    "fixed_versions": ["1.30.3", "2.1.15", "2.4.4", "3.10.4", "5.6.0"],
    "reproduce_version": "1.30.2",
    "verify_fixed_version": "1.30.3",
    "repo_url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
    "code_root": "external/phpspreadsheet",
    "ecosystem": "Packagist",
    "package": "phpoffice/phpspreadsheet",
    "install_recipe": "In a scratch directory run `composer require phpoffice/phpspreadsheet:1.30.2` (vulnerable) or `composer require phpoffice/phpspreadsheet:1.30.3` (fixed). The Packagist releases are the canonical artifacts; the matching git tags on the repo are equivalent.",
    "attacker_access": "Any code path that forwards an attacker-influenced filename string into IOFactory::load() or IOFactory::identify().",
    "exploit_outline": "For a clean, deterministic observable, use the SSRF vector. Start a local TCP listener on 127.0.0.1:<port>. Call IOFactory::load() (or IOFactory::identify()) with an `ftp://127.0.0.1:<port>/x` path. File::assertFile()'s is_file() check passes for the ftp:// wrapper, so the library proceeds to open the path and the local listener receives an inbound connection. The same is_file() gap means a `phar://` path is accepted and its archive metadata is deserialized.",
    "vulnerable_indicator": "On 1.30.2 the local listener on 127.0.0.1:<port> receives a connection when IOFactory::load()/identify() is called with the ftp:// path - the library made an outbound request to an attacker-chosen host (SSRF). The same flaw lets a phar:// path reach PHAR metadata deserialization.",
    "fixed_indicator": "On 1.30.3 the disallowed stream wrapper is rejected before any connection is made; the local listener receives no connection and the call fails with a validation error instead.",
    "reproduction_requirements": "PHP with composer and the phpoffice/phpspreadsheet package at the specified version. A local TCP listener on 127.0.0.1 for the SSRF observable. No external network, database, or browser required - the connection difference is observable on localhost.",
    "notes": "Two impacts share one root cause: the SSRF vector (ftp://) gives the cleanest deterministic observable and is the recommended PoC path, but the same File::assertFile() / is_file() gap also enables phar:// deserialization (object injection). State both in the reproduction."
  }
}
