Skip to content

Security updates#87

Open
h3xxit wants to merge 1 commit into
mainfrom
dev
Open

Security updates#87
h3xxit wants to merge 1 commit into
mainfrom
dev

Conversation

@h3xxit

@h3xxit h3xxit commented Jun 14, 2026

Copy link
Copy Markdown
Member

Summary by cubic

Hardened network security across HTTP, GraphQL, and WebSocket protocols to block SSRF and MITM. Enforces secure URL schemes, validates each redirect hop, and checks OAuth2 token URLs; adds tests and minor version bumps.

  • Bug Fixes

    • Replace prefix checks with hostname-based validators: allow https:///wss://, and only loopback for http:///ws://.
    • Add safe_request_with_redirects for manual discovery, tool calls, and OAuth2; disable redirects for SSE/streaming and gql transport.
    • Validate OAuth2 token_url at runtime and during OpenAPI conversion; reject internal or plain-HTTP non-loopback URLs.
    • WebSocket now enforces “WSS or loopback” and blocks upgrade redirects.
    • New tests cover redirect chains, token URL validation, and localhost-prefix bypass.
  • Dependencies

    • Bump utcp-http to 1.1.4, utcp-gql to 1.1.1, utcp-websocket to 1.1.1.
    • Add aiohttp>=3.8 to utcp-gql.

Written for commit fc3268e. Summary will update on new commits.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 16 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="plugins/communication_protocols/websocket/src/utcp_websocket/websocket_communication_protocol.py">

<violation number="1" location="plugins/communication_protocols/websocket/src/utcp_websocket/websocket_communication_protocol.py:239">
P1: `allow_redirects` is not a valid `ws_connect` argument, so connection setup now fails at runtime. This breaks all WebSocket manual registration/tool calls that reach this code path.</violation>
</file>

<file name="plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py">

<violation number="1" location="plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py:225">
P2: `ClientSession` is not closed on successful SSE streams; only error paths call `await session.close()`. This can accumulate unclosed connections during normal streaming usage.</violation>
</file>

<file name="plugins/communication_protocols/http/src/utcp_http/openapi_converter.py">

<violation number="1" location="plugins/communication_protocols/http/src/utcp_http/openapi_converter.py:379">
P2: This rejects valid OpenAPI 3.0 OAuth2 specs that use relative `tokenUrl`. Guard security validation to absolute URLs (or resolve first) instead of failing conversion on relative URLs.</violation>
</file>

<file name="plugins/communication_protocols/http/src/utcp_http/_security.py">

<violation number="1" location="plugins/communication_protocols/http/src/utcp_http/_security.py:212">
P1: Redirect handling forwards credentials to new origins because auth-related kwargs are never scrubbed between hops. This can leak API keys/bearer/basic credentials and cookies to attacker-controlled HTTPS redirect targets.</violation>
</file>

<file name="plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py">

<violation number="1" location="plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py:141">
P1: Redirect protection is applied too late: it runs after entering the GqlClient context, but schema fetch can already happen during `__aenter__`. This leaves an SSRF/credential-leak path on the initial GraphQL request.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

# aiohttp's ws_connect defaults to following HTTP
# redirects on the upgrade handshake; refuse so a
# 3xx response cannot land us on a different host.
allow_redirects=False,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: allow_redirects is not a valid ws_connect argument, so connection setup now fails at runtime. This breaks all WebSocket manual registration/tool calls that reach this code path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/communication_protocols/websocket/src/utcp_websocket/websocket_communication_protocol.py, line 239:

<comment>`allow_redirects` is not a valid `ws_connect` argument, so connection setup now fails at runtime. This breaks all WebSocket manual registration/tool calls that reach this code path.</comment>

<file context>
@@ -198,7 +232,11 @@ async def _get_connection(self, call_template: WebSocketCallTemplate) -> ClientW
+                # aiohttp's ws_connect defaults to following HTTP
+                # redirects on the upgrade handshake; refuse so a
+                # 3xx response cannot land us on a different host.
+                allow_redirects=False,
             )
             self._connections[provider_key] = ws
</file context>

current_method = "GET"
kwargs.pop("json", None)
kwargs.pop("data", None)
current_url = next_url

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Redirect handling forwards credentials to new origins because auth-related kwargs are never scrubbed between hops. This can leak API keys/bearer/basic credentials and cookies to attacker-controlled HTTPS redirect targets.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/communication_protocols/http/src/utcp_http/_security.py, line 212:

<comment>Redirect handling forwards credentials to new origins because auth-related kwargs are never scrubbed between hops. This can leak API keys/bearer/basic credentials and cookies to attacker-controlled HTTPS redirect targets.</comment>

<file context>
@@ -110,3 +111,108 @@ def ensure_secure_url(url: str, *, context: Optional[str] = None) -> None:
+                current_method = "GET"
+                kwargs.pop("json", None)
+                kwargs.pop("data", None)
+            current_url = next_url
+            hops += 1
+
</file context>

headers = await self._prepare_headers(manual_call_template)
transport = AIOHTTPTransport(url=manual_call_template.url, headers=headers)
async with GqlClient(transport=transport, fetch_schema_from_transport=True) as session:
self._disable_transport_redirects(transport)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Redirect protection is applied too late: it runs after entering the GqlClient context, but schema fetch can already happen during __aenter__. This leaves an SSRF/credential-leak path on the initial GraphQL request.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/communication_protocols/gql/src/utcp_gql/gql_communication_protocol.py, line 141:

<comment>Redirect protection is applied too late: it runs after entering the GqlClient context, but schema fetch can already happen during `__aenter__`. This leaves an SSRF/credential-leak path on the initial GraphQL request.</comment>

<file context>
@@ -94,17 +100,45 @@ async def _prepare_headers(
             headers = await self._prepare_headers(manual_call_template)
             transport = AIOHTTPTransport(url=manual_call_template.url, headers=headers)
             async with GqlClient(transport=transport, fetch_schema_from_transport=True) as session:
+                self._disable_transport_redirects(transport)
                 schema = session.client.schema
                 tools: List[Tool] = []
</file context>

method, url, params=query_params, headers=request_headers,
auth=auth, cookies=cookies, json=json_data, data=data, timeout=None
auth=auth, cookies=cookies, json=json_data, data=data,
timeout=None, allow_redirects=False,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: ClientSession is not closed on successful SSE streams; only error paths call await session.close(). This can accumulate unclosed connections during normal streaming usage.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py, line 225:

<comment>`ClientSession` is not closed on successful SSE streams; only error paths call `await session.close()`. This can accumulate unclosed connections during normal streaming usage.</comment>

<file context>
@@ -208,10 +212,26 @@ async def call_tool_streaming(self, caller, tool_name: str, tool_args: Dict[str,
                 method, url, params=query_params, headers=request_headers,
-                auth=auth, cookies=cookies, json=json_data, data=data, timeout=None
+                auth=auth, cookies=cookies, json=json_data, data=data,
+                timeout=None, allow_redirects=False,
             )
+            if 300 <= response.status < 400:
</file context>

Comment on lines +379 to +381
ensure_secure_url(
token_url, context="OAuth2 tokenUrl in OpenAPI spec"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This rejects valid OpenAPI 3.0 OAuth2 specs that use relative tokenUrl. Guard security validation to absolute URLs (or resolve first) instead of failing conversion on relative URLs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/communication_protocols/http/src/utcp_http/openapi_converter.py, line 379:

<comment>This rejects valid OpenAPI 3.0 OAuth2 specs that use relative `tokenUrl`. Guard security validation to absolute URLs (or resolve first) instead of failing conversion on relative URLs.</comment>

<file context>
@@ -368,6 +368,17 @@ def _create_auth_from_scheme(self, scheme: Dict[str, Any], scheme_name: str) ->
+                            # runtime check in ``_handle_oauth2`` also
+                            # enforces this -- see
+                            # GHSA-8cp3-qxj6-px34.
+                            ensure_secure_url(
+                                token_url, context="OAuth2 tokenUrl in OpenAPI spec"
+                            )
</file context>
Suggested change
ensure_secure_url(
token_url, context="OAuth2 tokenUrl in OpenAPI spec"
)
parsed_token_url = urlparse(token_url)
if parsed_token_url.scheme and parsed_token_url.netloc:
ensure_secure_url(
token_url, context="OAuth2 tokenUrl in OpenAPI spec"
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant