GHSA-9qhg-99ww-9mqc
utcp-http SSRF: HTTP tool invocation follows redirects without re-validating the target
Summary
## Summary `HttpCommunicationProtocol.call_tool` validates only the pre-redirect tool URL, then issues the request with redirects enabled and never re-checks where it lands. A tool whose endpoint is an attacker-controlled public URL can therefore `302`-redirect the UTCP client into an internal service including the cloud metadata endpoint and the response body is returned to the tool caller. This is a working SSRF + internal-data-exfiltration primitive. This is the redirect invariant of the SSRF class fixed in GHSA-39j6-4867-gg4w; that fix added an invocation-time URL check but left the redirect hop unguarded. This vector bypasses the GHSA-39j6-4867-gg4w mitigation via unvalidated redirects. ## Root cause 1. The resolved URL is validated once, before the request: https://github.com/universal-tool-calling-protocol/python-utcp/blob/4ed0a48b84a452338bd3e996efb0d169e8d75ac2/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py#L281 2. The request is then made with aiohttp's default `allow_redirects=True` and no per-hop revalidation, so the redirect target bypasses the check entirely: https://github.com/universal-tool-calling-protocol/python-utcp/blob/4ed0a48b84a452338bd3e996efb0d169e8d75ac2/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py#L313-L332 The validator (`_security.py`) blocks plain-HTTP to non-loopback hosts, exactly the metadata/internal case, but only the first hop ever reaches it. ## Reachability Triggered whenever the host registers a tool/manual whose endpoint URL is attacker-influenced (e.g. a manual or OpenAPI spec discovered from a runtime-supplied URL: a core UTCP usage pattern) and that tool is then called. The initial URL only has to pass the validator (any `https://`, or a benign host the attacker controls); the attacker's server supplies the redirect. No special configuration is required. ## Preconditions - The attacker controls the server the tool points at - either the registered tool/manual endpoint URL is attacker-influenced (e.g. a manual/OpenAPI spec discovered from a runtime-supplied URL), or a legitimate endpoint the tool already points at is attacker-controlled or compromised. - The initial tool URL passes `ensure_secure_url` — trivially met by any `https://` URL or a benign attacker-owned host; the attacker only needs to return a `3xx` `Location`. - The tool is invoked (`call_tool`) after registration. - An internal HTTP service is reachable from the UTCP process and returns useful data on an unauthenticated `GET` (cloud metadata, internal admin panel, unauth datastore, link-local endpoint). - The tool's return value is surfaced back to the caller/agent (the usual agentic flow), giving the attacker the response body. - For the IAM-credential outcome specifically: the host runs on a cloud instance with **IMDSv1** enabled. IMDSv2-only hosts block this exact result (it needs a `PUT` for a session token), but other internal-SSRF targets remain reachable. ## PoC The validator rejects the internal targets directly, but the redirect from an allowed tool URL reaches one anyway and returns its body. Runs the real released `HttpCommunicationProtocol`; the "metadata" service is bound on a non-loopback LAN IP, which the validator rejects exactly like `169.254.169.254`. Run: `pip install utcp-http==1.1.3 aiohttp && python poc.py` ```python import asyncio, socket from aiohttp import web from utcp_http.http_communication_protocol import HttpCommunicationProtocol from utcp_http.http_call_template import HttpCallTemplate MD = "/latest/meta-data/iam/security-credentials/app-role" STOLEN = {"Code": "Success", "AccessKeyId": "ASIAEXAMPLESTOLENKEY", "SecretAccessKey": "wJalr/EXAMPLE/STOLEN/SECRET", "Token": "Fwo...session"} def lan_ip(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect(("8.8.8.8", 80)); return s.getsockname()[0] finally: s.close() async def main(): internal = lan_ip() me
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| PyPI | utcp-http | — | 1.1.4 |
Remediation: Upgrade to 1.1.4 or later.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.