CVE-2026-108360
Mở trang đầy đủCVSS 6.4 MEDIUM Truy cập trái phép Lộ dữ liệu
PhpSpreadsheet: SSRF whitelist bypass via unre-validated HTTP redirect in Drawing::setPath external image loading
Mô tả
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
Điểm và phân loại
- CVSS
- 6.4 MEDIUM v3.1
- Nguồn điểm
- GitHub Advisory
- Mức độ (theo nguồn)
- MEDIUM
- EPSS
- 0.34%
Vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N
Ảnh hưởng
| Package | Bản bị ảnh hưởng | Bản đã sửa |
|---|---|---|
composer phpoffice/phpspreadsheet |
>= 4.0.0, <= 5.9.0 |
5.10.0
|
composer phpoffice/phpspreadsheet |
>= 3.3.0, <= 3.10.7 |
3.10.8
|
composer phpoffice/phpspreadsheet |
>= 2.2.0, <= 2.4.7 |
2.4.8
|
composer phpoffice/phpspreadsheet |
>= 2.0.0, <= 2.1.18 |
2.1.19
|
composer phpoffice/phpspreadsheet |
<= 1.30.6 |
1.30.7
|
composer phpoffice/phpspreadsheet |
>= 4.0.0, < 5.10.0 |
5.10.0
|
composer phpoffice/phpspreadsheet |
>= 3.3.0, < 3.10.8 |
3.10.8
|
composer phpoffice/phpspreadsheet |
>= 2.2.0, < 2.4.8 |
2.4.8
|
composer phpoffice/phpspreadsheet |
>= 2.0.0, < 2.1.19 |
2.1.19
|
composer phpoffice/phpspreadsheet |
< 1.30.7 |
1.30.7
|
Vì sao bị lọc
- CVSS 6.4 < 8.0 và không thuộc CISA KEV
Loại tác động
- Truy cập trái phép
- CWE-863
- Lộ dữ liệu
- CWE-918; từ khoá "ssrf"
Dữ liệu riêng của nguồn
- OSV.dev · ID
- GHSA-2phh-27fw-q4gc
Tham khảo
- https://github.com/advisories/GHSA-2phh-27fw-q4gc
- https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-2phh-27fw-q4gc
- https://nvd.nist.gov/vuln/detail/CVE-2026-108360
- https://github.com/PHPOffice/PhpSpreadsheet/commit/299993894dce37aa3b64aa23e9a3f4bb77ab0e66
- https://github.com/PHPOffice/PhpSpreadsheet/commit/94590951d76a250a5fc1ecf69a0b3f7439a9af49
- https://github.com/PHPOffice/PhpSpreadsheet/commit/980b5c294493856f1a25754f1e8a1d2185b93738
- https://github.com/PHPOffice/PhpSpreadsheet/commit/9a0b48622ebfe93fa48cb9bc09ce57303bb07af9
- https://github.com/PHPOffice/PhpSpreadsheet/commit/da13f40e8760d599b0e8a296017d4b028b20e86c
- https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/1.30.7
- https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.1.19
- https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.4.8
- https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/3.10.8
- https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/5.10.0
- https://osv.dev/vulnerability/GHSA-2phh-27fw-q4gc
- https://github.com/PHPOffice/PhpSpreadsheet