CVE-2026-108360

mediumpriority · track · 0OT risk · elevatedEPSS 0.002 · 13 %ileUS
grimoire priorityP3 · Track
Exploitable?
—
no known exploitation
Likely / severe?
0.22% (13th) EPSS
CVSS 6.4 · medium
Is it mine?
5 products
Check your exposure
Who says so?
1 sources
osv

EPSS · scored 2026-10-27

PhpSpreadsheet is a pure PHP library for reading and writing spreadsheet files. Prior to 1.30.7, 2.1.19, 2.4.8, 3.10.8, and 5.10.0, Worksheet\Drawing::setPath validates only the initial external-image URL with setIsWhitelisted when setAllowExternalImages is enabled. The HTTP stream context permits PHP to follow a redirect without applying the callback to the redirected URL, so a permitted image host can redirect the server to an internal address and cause a blind GET request. The response body is not returned to the attacker because the reader validates the downloaded content as an image and removes the temporary file, but internal GET-driven endpoints, network reachability, and cloud metadata endpoints may be reached. This issue is fixed in versions 1.30.7, 2.1.19, 2.4.8, 3.10.8, and 5.10.0.

01

cvss

Technical severity, 0–10. One row per scoring source.

6.4
010

base score 0–10 · higher = more severe · hover a vector to decode

v3.16.4mediumCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:Nsecondary · security-advisories@github.comexploitability 3.1 · impact 2.7
v3.16.4mediumCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:Nsecondary · OSV
v3.15.9mediumCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N/E:Usecondary · CVSS-BT (t0sche)
02

weaknesses

The CWE weakness classes this flaw belongs to.

03

affected products (5)

Products and version ranges known to be affected.

04

advisories (1)

Vendor and ecosystem advisories describing this.

  • GHSA-2phh-27fw-q4gcPhpSpreadsheet: SSRF whitelist bypass via unre-validated HTTP redirect in Drawing::setPath external image loading2026-10-27ghsa · osvUS

    ## 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`: `<Relationship ... Type=".../image" Target="https://trusted-whitelisted.example/redir.png" TargetMode="External"/>`. 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: `<img src="https://trusted-whitelisted.example/redir.png">` 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, <= 5.9.0`. The `isWhitelisted` control being bypassed was introduced in 5.5.0 (commit `50a7552f`); present and unfixed on the latest release (5.9.0) and on HEAD (`05e99ebf`). No post-release fix to `Drawing.php`. ## Suggested Fix Add `'follow_location' => 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

05

sources

Which sources assert this record, and when.

published 2026-10-22modified 2026-10-23
07

references (11) nvd

External citations — patches, advisories, analysis.

scry

Attackers weaponise vulnerabilities like this against internet-exposed services. See what is exposed on your perimeter with a free passive scan.

Free scan