GHSA-2phh-27fw-q4gc CVE-2026-108360
PhpSpreadsheet: SSRF whitelist bypass via unre-validated HTTP redirect in Drawing::setPath external image loading
- Severity
- 6.4 / 10
- Medium · CVSS 3.1
- Exploit chance (EPSS)
- 0.34%
- Chance of exploitation in the next 30 days. Higher than 27.35% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Fix
5.10.0- Upgrade phpoffice/phpspreadsheet to this version or later
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):/) permitshttp/https(the redirect-following schemes) and runs on the initial URL only. - Opt-in gate
if (!$allowExternal)at:128(requiressetAllowExternalImages(true); defaultfalse,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-152setsuser_agent+header(+ssl.crypto_methodfor https) but omitsfollow_location.git grep follow_locationon tag 5.9.0 yields a single hit —Service.php:45(the CVE-2026-59931 fix) — confirming it was never added toDrawing.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 unlinked — 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
- Attacker server
https://trusted-whitelisted.example/redir.pngreturns302 Location: http://169.254.169.254/latest/meta-data/. evil.xlsxwithxl/drawings/_rels/drawing1.xml.rels:<Relationship ... Type=".../image" Target="https://trusted-whitelisted.example/redir.png" TargetMode="External"/>.- 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 viaReader\Html(Html.php:1237) with the same reader settings.
Attack Chain
- Entry: crafted
evil.xlsx,xl/drawings/_rels/*.relsTargetMode="External",Target="https://trusted.example/redir.png"(whitelisted host). Guard: none (legal external-image relationship). Bypass proof: Target stored verbatim ->$urlatReader/Xlsx.php:1544. - Config: victim loads with
setAllowExternalImages(true)->setIsWhitelisted($cb)where$cballowstrusted.example. Guard:Drawing.php:128opt-in (satisfied) +:131whitelist. Bypass proof: initial URL is whitelisted -> returns true; check runs once on$path. - Fetch/redirect:
file_get_contents(:154) GETs the URL; whitelisted host returns302 Location: http://169.254.169.254/.... Guard: stream context (:140-152). Bypass proof: nofollow_locationkey -> PHP follows the redirect (defaultfollow_location=1,max_redirects=20) with no re-validation. - 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 atService.php:45;Drawing.phphas none.- CVE-2026-59931's own regression harness (
ServerTest.php+redirect.php: a whitelisted host 302-redirects todirect.php; only afterfollow_location => 0does the fetch stop following it) demonstrates the exact behavior. That fix is present inService.phpand absent inDrawing.php. git grep isWhitelisted 5.9.0 -- src/confirms the callback is applied only atDrawing.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
Affected versions
Packagist
| Package | Vulnerable | Fixed in |
|---|---|---|
| phpoffice/phpspreadsheet | >= 4.0.0, < 5.10.0 |
5.10.0 |
| phpoffice/phpspreadsheet | >= 3.3.0, < 3.10.8 |
3.10.8 |
| phpoffice/phpspreadsheet | >= 2.2.0, < 2.4.8 |
2.4.8 |
| phpoffice/phpspreadsheet | >= 2.0.0, < 2.1.19 |
2.1.19 |
| phpoffice/phpspreadsheet | < 1.30.7 |
1.30.7 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
phpoffice/phpspreadsheet Packagist → 5.10.0
- composer
composer require phpoffice/phpspreadsheet:^5.10.0
Severity breakdown
CVSS 3.1 6.4
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- Low
- User interaction
- None
- Scope
- Changed
- Confidentiality
- Low
- Integrity
- Low
- Availability
- None
Weakness
CWE-863: Incorrect Authorization
The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.
How to prevent it
- Architecture and Design: Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries. Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
- Architecture and Design: Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].
- Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. For example, consider using authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
- Architecture and Design: For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page. One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
- System Configuration, Installation: Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.
CWE-918: Server-Side Request Forgery (SSRF)
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
References
Advisories
Packages
Other links
- github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-2phh-27fw-q4gc
- github.com/PHPOffice/PhpSpreadsheet/commit/299993894dce37aa3b64aa23e9a3f4bb77ab…
- github.com/PHPOffice/PhpSpreadsheet/commit/94590951d76a250a5fc1ecf69a0b3f7439a9…
- github.com/PHPOffice/PhpSpreadsheet/commit/980b5c294493856f1a25754f1e8a1d2185b9…
- github.com/PHPOffice/PhpSpreadsheet/commit/9a0b48622ebfe93fa48cb9bc09ce57303bb0…
- github.com/PHPOffice/PhpSpreadsheet/commit/da13f40e8760d599b0e8a296017d4b028b20…
- github.com/PHPOffice/PhpSpreadsheet/releases/tag/1.30.7
- github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.1.19
- github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.4.8
- github.com/PHPOffice/PhpSpreadsheet/releases/tag/3.10.8
- github.com/PHPOffice/PhpSpreadsheet/releases/tag/5.10.0
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-j2w7-387v-m2rq PhpSpreadsheet ODS Reader: Unbounded number-rows-repeated → CPU & Memory Exhaustion (DoS) | High | Also affects phpoffice/phpspreadsheet |
| GHSA-3x94-m842-937c PhpSpreadsheet: XmlScanner Bypass via ISO-2022 Escape Sequences Allows XXE and Denial of Service | High | Also affects phpoffice/phpspreadsheet |
| GHSA-rxj4-h86g-25gc PhpSpreadsheet: XmlScanner Bypass via Double-Encoded unentity() TOCTOU Allows XXE and Denial of Service | High | Also affects phpoffice/phpspreadsheet |
| GHSA-xh5m-36r6-47m3 PHPSpreadsheet: XLS/OLE sector-chain self-loop causes memory exhaustion | High | Also affects phpoffice/phpspreadsheet |
| GHSA-2mrg-gjxq-2gvr PHPSpreadsheet: Gnumeric reader unbounded gzip expansion causes memory exhaustion | High | Also affects phpoffice/phpspreadsheet |
| GHSA-6hq5-7373-42rg PHPSpreadsheet: SSRF bypass via HTTP redirect in WEBSERVICE() domain whitelist | High | Also affects phpoffice/phpspreadsheet |
| GHSA-87m4-826x-3crx PHPSpreadsheet has a patch bypass for CVE-2026-34084 | Critical | Also affects phpoffice/phpspreadsheet |
| GHSA-7c6m-4442-2x6m PhpSpreadsheet has CPU Denial of Service via Unbounded Row Number in XLSX Row Dimensions | High | Also affects phpoffice/phpspreadsheet |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor