GitHub Advisoryadvisory · Packagist
GHSA-2phh-27fw-q4gc PhpSpreadsheet: SSRF whitelist bypass via unre-validated HTTP redirect in Drawing::setPath external image loading
MEDIUM
Official page ↗
- Aliases
- CVE-2026-108360
- Ecosystem
- Packagist
- Packages
- phpoffice/phpspreadsheet
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N
- Weaknesses
- CWE-863, CWE-918
- Reviewed
- 2026-10-27
Affected packages
• Packagist phpoffice/phpspreadsheet: >= 4.0.0, < 5.10.0 → fixed in 5.10.0
• Packagist phpoffice/phpspreadsheet: >= 3.3.0, < 3.10.8 → fixed in 3.10.8
• Packagist phpoffice/phpspreadsheet: >= 2.2.0, < 2.4.8 → fixed in 2.4.8
• Packagist phpoffice/phpspreadsheet: >= 2.0.0, < 2.1.19 → fixed in 2.1.19
• Packagist phpoffice/phpspreadsheet: < 1.30.7 → fixed in 1.30.7
Description
Summary
PhpSpreadsheet's `Worksheet\Drawing::setPath()` validates an external-image URL against an operator-supplied `isWhitelisted` callback once, on the initial URL only, then fetches it with `file_get_contents()` using a stream context that does not set `follow_location => 0`. A whitelisted host that returns an HTTP `302 Location:` redirect to an internal target is therefore followed by PHP's HTTP wrapper (default `follow_location=1`, `max_redirects=20`) with no re-validation of the redirect hop — defeating the whitelist and yielding a blind SSRF from the server.
This is an unfixed variant of CVE-2026-59931 (GHSA-6hq5-7373-42rg, the WEBSERVICE redirect-whitelist-bypass): the `follow_location => 0` fix (commit `7ef7b25e`) was applied ONLY to `Calculation/Web/Service.php`; `Worksheet/Drawing.php` was never touched.
Root Cause
`src/PhpSpreadsheet/Worksheet/Drawing.php`:
• Scheme allowlist at `:125` (`/^(http|https|file|ftp|s3):/`) permits `http`/`https` (the redirect-following schemes) and runs on the initial URL only.
• Opt-in gate `if (!$allowExternal)` at `:128` (requires `setAllowExternalImages(true)`; default `false`, `BaseReader.php:55`).
• Whitelist check `if ($isWhitelisted !== null && !$isWhitelisted($path))` at `:131` — validates the INITIAL URL only; never re-applied after a redirect.
• Stream context built at `:140-152` sets `user_agent` + `header` (+ `ssl.crypto_method` for https) but omits `follow_location`. `git grep follow_location` on tag 5.9.0 yields a single hit — `Service.php:45` (the CVE-2026-59931 fix) — confirming it was never added to `Drawing.php`.
• Sink `file_get_contents($path, false, $ctx)` at `:154`.
Impact
Blind SSRF from the server: reach and trigger internal GET-driven endpoints, coarse internal reachability/port probing, and reach cloud-metadata endpoints. The fetched body is written to a temp file, checked with `isImage()` (mime `image/*`, or `application/octet-stream` with `bin`/`emf` extension), only image dimensions are stored, and the temp file is `unlink`ed — so the response body is not exfiltrated (non-image responses fail `isImage()`). This is a blind GET primitive, not full response read.
Proof of Concept
1. Attacker server `https://trusted-whitelisted.example/redir.png` returns `302 Location: http://169.254.169.254/latest/meta-data/`.
2. `evil.xlsx` with `xl/drawings/_rels/drawing1.xml.rels`: ``.
3. Victim: `$r = new Xlsx(); $r->setAllowExternalImages(true)->setIsWhitelisted(fn($p)=>str_contains($p,'trusted-whitelisted.example')); $r->load('evil.xlsx');` -> the server-side GET is forced to the internal target despite the whitelist.
HTML-reader variant: `` loaded via `Reader\Html` (`Html.php:1237`) with the same reader settings.
Attack Chain
1. Entry: crafted `evil.xlsx`, `xl/drawings/_rels/*.rels` `TargetMode="External"`, `Target="https://trusted.example/redir.png"` (whitelisted host). Guard: none (legal external-image relationship). Bypass proof: Target stored verbatim -> `$url` at `Reader/Xlsx.php:1544`.
2. Config: victim loads with `setAllowExternalImages(true)->setIsWhitelisted($cb)` where `$cb` allows `trusted.example`. Guard: `Drawing.php:128` opt-in (satisfied) + `:131` whitelist. Bypass proof: initial URL is whitelisted -> returns true; check runs once on `$path`.
3. Fetch/redirect: `file_get_contents` (`:154`) GETs the URL; whitelisted host returns `302 Location: http://169.254.169.254/...`. Guard: stream context (`:140-152`). Bypass proof: no `follow_location` key -> PHP follows the redirect (default `follow_location=1`, `max_redirects=20`) with no re-validation.
4. Impact: server issues a blind GET to the internal target (body temp-filed, `isImage()`-checked, dimensions stored, temp file unlinked — body not exfiltrated).
Bypass Evidence
• `git grep follow_location 5.9.0 -- src/` -> single hit at `Service.php:45`; `Drawing.php` has none.
• CVE-2026-59931's own regression harness (`ServerTest.php` + `redirect.php`: a whitelisted host 302-redirects to `direct.php`; only after `follow_location => 0` does the fetch stop following it) demonstrates the exact behavior. That fix is present in `Service.php` and absent in `Drawing.php`.
• `git grep isWhitelisted 5.9.0 -- src/` confirms the callback is applied only at `Drawing.php:131` (initial `$path`), never after the redirect.
Affected Versions
`>= 5.5.0, 0` (and/or `'max_redirects' => 0`) to `$ctxArray['http']` in `Drawing::setPath()` (`:140-148`), mirroring the CVE-2026-59931 fix at `Service.php:45`; alternatively follow redirects manually and re-run `$isWhitelisted` on each hop.
---
Reported by zx (Jace) — GitHub: @manus-use
Related
Alias · CVE (1)
- CVE-2026-108360 PhpSpreadsheet is a pure PHP library for reading and writing spreadsheet files. vulnerability
References
- https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-2phh-27fw-q4gc web
- https://nvd.nist.gov/vuln/detail/CVE-2026-108360 advisory
- https://github.com/PHPOffice/PhpSpreadsheet/commit/299993894dce37aa3b64aa23e9a3f4bb77ab0e66 web
- https://github.com/PHPOffice/PhpSpreadsheet/commit/94590951d76a250a5fc1ecf69a0b3f7439a9af49 web
- https://github.com/PHPOffice/PhpSpreadsheet/commit/980b5c294493856f1a25754f1e8a1d2185b93738 web
- https://github.com/PHPOffice/PhpSpreadsheet/commit/9a0b48622ebfe93fa48cb9bc09ce57303bb07af9 web
- https://github.com/PHPOffice/PhpSpreadsheet/commit/da13f40e8760d599b0e8a296017d4b028b20e86c web
- https://github.com/PHPOffice/PhpSpreadsheet package
- https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/1.30.7 web
- https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.1.19 web
- https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.4.8 web
- https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/3.10.8 web
- https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/5.10.0 web