{"id":"PYSEC-2026-3396","summary":"utcp-http vulnerable to SSRF via attacker-controlled OpenAPI servers[0].url in HTTP communication protocol","details":"## Summary\n\nThe `utcp-http` plugin is vulnerable to a blind Server-Side Request Forgery (SSRF) caused by a trust-boundary inconsistency between manual discovery and tool invocation. `register_manual()` validates the discovery URL against an HTTPS / loopback allowlist, but `call_tool()` and `call_tool_streaming()` reuse the resolved `tool_call_template.url` directly without revalidating. An attacker who hosts a malicious OpenAPI spec on a legitimate HTTPS endpoint can declare `servers: [{ url: \"http://169.254.169.254\" }]` (or any internal address) in the spec; the OpenAPI converter blindly trusts that value and the tool becomes a blind SSRF primitive that exposes cloud metadata, internal services, and other firewalled-only endpoints to the LLM caller.\n\nAll three HTTP-class protocols (`utcp_http.http`, `utcp_http.streamable_http`, `utcp_http.sse`) shared the same gap, plus a separate prefix-bypass: the previous `startswith(\"http://localhost\")` check let URLs like `http://localhost.evil.com` through.\n\n## Impact\n\nA remote attacker who can convince the agent (via the LLM context, prompt injection, or a tool-discovery surface) to register their HTTPS OpenAPI URL can:\n- Map internal networks behind the agent.\n- Read AWS/GCP IAM credentials from cloud metadata endpoints (`http://169.254.169.254`, `http://metadata.google.internal`).\n- Reach unauthenticated internal services (Elasticsearch, Redis HTTP, internal admin panels).\n- Have responses returned to the LLM, which combined with prompt injection enables exfiltration back to the attacker.\n\n## Affected versions\n\n`utcp-http \u003c= 1.1.1`.\n\n## Patched versions\n\n`utcp-http 1.1.2`.\n\n## Patch\n\nCommit: 5b16e43 on `dev`.\n\n- New `utcp_http._security` helper: `ensure_secure_url(url, context=...)` parses the URL with `urllib.parse.urlparse` and validates the hostname (not a string prefix) against the loopback set, closing the `localhost.evil.com` bypass.\n- All three protocols call `ensure_secure_url(url, context=\"manual discovery\")` in `register_manual` (replacing the duplicated prefix check) and `ensure_secure_url(url, context=\"tool invocation\")` immediately before each aiohttp request in `call_tool` / `call_tool_streaming`. The runtime check is the actual SSRF fix.\n- New regression tests in `test_security.py` pin the accept/reject decisions and explicitly cover the historical bypass cases.\n\n## Workarounds\n\nFor users who cannot upgrade immediately:\n- Refuse to call `register_manual` with any URL controlled by an untrusted party, even over HTTPS.\n- Restrict outbound network access from the host running the agent so internal addresses (RFC1918, 169.254.0.0/16, loopback for cloud metadata) are unreachable.\n\n## Credit\n\nDiscovered and reported by [@YLChen-007](https://github.com/YLChen-007) in #83.","aliases":["CVE-2026-44661","GHSA-39j6-4867-gg4w"],"modified":"2026-07-13T16:33:27.692394107Z","published":"2026-07-13T15:15:41.228322Z","references":[{"type":"WEB","url":"https://github.com/universal-tool-calling-protocol/python-utcp/security/advisories/GHSA-39j6-4867-gg4w"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-44661"},{"type":"PACKAGE","url":"https://github.com/universal-tool-calling-protocol/python-utcp"},{"type":"PACKAGE","url":"https://pypi.org/project/utcp-http"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-39j6-4867-gg4w"}],"affected":[{"package":{"name":"utcp-http","ecosystem":"PyPI","purl":"pkg:pypi/utcp-http"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.1.2"}]}],"versions":["1.0.0","1.0.1","1.0.2","1.0.3","1.0.4","1.0.5","1.1.0","1.1.1"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/utcp-http/PYSEC-2026-3396.yaml"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N"}]}