Skip to content

Commit 1810409

Browse files
committed
fix(security): drop URL substring checks for WXO auth scheme selection
Closes CodeQL alerts #201-204 (py/incomplete-url-substring-sanitization, high). ``get_authenticator`` chose between the IBM Cloud (IAM) and MCSP authenticators by testing ``".cloud.ibm.com" in instance_url`` / ``".ibm.com" in instance_url`` against the raw URL string. Because the check scanned the whole URL, a provider URL whose hostname passes the existing ``.ibm.com`` allowlist (e.g. ``https://api.sso.ibm.com``) but whose path, userinfo, query, or fragment contains ``.cloud.ibm.com`` could flip the wrong authenticator: https://api.sso.ibm.com/.cloud.ibm.com → wrongly IAM https://cloud.ibm.com@attacker.example/… → wrongly IAM https://host.ibm.com/?trick=.cloud.ibm.com → wrongly IAM Scheme selection now drives off ``urlparse(instance_url).hostname`` with ``hostname == "…"`` or ``hostname.endswith(".…")`` — the exact pattern documented as safe in CodeQL's rule help. Also tightens the two test assertions flagged by the same rule (``"cloud.ibm.com" in result.base_url``) to exact URL equality against the normalised mapper output, and adds a parametrised regression test covering the five known substring bypass shapes (path / query / userinfo / fragment smuggling). No behavioural change for real IBM Cloud / MCSP URLs: all 14 ``get_authenticator`` tests and 403 tests across the two touched test modules pass.
1 parent dc5b27e commit 1810409

3 files changed

Lines changed: 35 additions & 5 deletions

File tree

src/backend/base/langflow/services/adapters/deployment/watsonx_orchestrate/client.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from contextvars import ContextVar
2020
from dataclasses import dataclass
2121
from typing import TYPE_CHECKING, ClassVar
22+
from urllib.parse import urlparse
2223

2324
from ibm_cloud_sdk_core.authenticators import IAMAuthenticator, MCSPAuthenticator
2425
from ibm_watsonx_orchestrate_core.types.connections import KeyValueConnectionCredentials
@@ -126,10 +127,16 @@ def set_request_context_provider_clients(*, provider_id: UUID, user_id: UUID | s
126127

127128

128129
def get_authenticator(instance_url: str, api_key: str) -> IAMAuthenticator | MCSPAuthenticator:
129-
"""Return the appropriate authenticator for the Watsonx Orchestrate API."""
130-
if ".cloud.ibm.com" in instance_url:
130+
"""Return the appropriate authenticator for the Watsonx Orchestrate API.
131+
132+
Scheme selection is driven by the *hostname* of ``instance_url`` — never by a
133+
raw substring match against the full URL string, which would let a URL like
134+
``https://evil.example.com/.cloud.ibm.com`` bypass the branching intent.
135+
"""
136+
hostname = (urlparse(instance_url).hostname or "").lower()
137+
if hostname == "cloud.ibm.com" or hostname.endswith(".cloud.ibm.com"):
131138
authenticator = IAMAuthenticator(apikey=api_key, url=WxOAuthURL.IBM_IAM.value)
132-
elif ".ibm.com" in instance_url:
139+
elif hostname == "ibm.com" or hostname.endswith(".ibm.com"):
133140
authenticator = MCSPAuthenticator(apikey=api_key, url=WxOAuthURL.MCSP.value)
134141
else:
135142
msg = f"Could not determine authentication scheme for instance URL: {instance_url}"

src/backend/tests/unit/api/v1/test_deployment_mapper_watsonx.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,7 +1809,7 @@ def test_wxo_mapper_verify_credentials_create_filters_non_credential_fields() ->
18091809
)
18101810
result = mapper.resolve_verify_credentials_for_create(payload=payload)
18111811
assert isinstance(result, VerifyCredentials)
1812-
assert "cloud.ibm.com" in result.base_url
1812+
assert result.base_url == "https://api.us-south.wxo.cloud.ibm.com/"
18131813
assert result.provider_data is not None
18141814
assert result.provider_data["api_key"] == "my-secret-key" # pragma: allowlist secret
18151815
assert "tenant_id" not in result.provider_data
@@ -1832,7 +1832,7 @@ def test_wxo_mapper_verify_credentials_create_accepts_missing_tenant() -> None:
18321832

18331833
result = mapper.resolve_verify_credentials_for_create(payload=payload)
18341834
assert isinstance(result, VerifyCredentials)
1835-
assert "cloud.ibm.com" in result.base_url
1835+
assert result.base_url == "https://api.us-south.wxo.cloud.ibm.com/"
18361836

18371837

18381838
def test_wxo_mapper_provider_account_create_requires_tenant() -> None:

src/backend/tests/unit/services/deployment/test_watsonx_orchestrate.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5165,6 +5165,29 @@ def test_get_authenticator_unknown_url():
51655165
get_authenticator("https://example.com", "test-key")
51665166

51675167

5168+
@pytest.mark.parametrize(
5169+
"bypass_url",
5170+
[
5171+
# Path smuggling: IBM domain appears in the path, not the hostname.
5172+
"https://evil.example.com/.cloud.ibm.com",
5173+
"https://evil.example.com/.ibm.com",
5174+
# Query-string smuggling: IBM domain in query parameters.
5175+
"https://evil.example.com/?host=.cloud.ibm.com",
5176+
# Userinfo smuggling: IBM domain in userinfo before @.
5177+
"https://cloud.ibm.com@evil.example.com/",
5178+
# Fragment smuggling.
5179+
"https://evil.example.com/#.cloud.ibm.com",
5180+
],
5181+
)
5182+
def test_get_authenticator_rejects_substring_bypass(bypass_url):
5183+
"""Regression for CodeQL py/incomplete-url-substring-sanitization: scheme selection must be hostname-based."""
5184+
from langflow.services.adapters.deployment.watsonx_orchestrate.client import get_authenticator
5185+
from lfx.services.adapters.deployment.exceptions import AuthSchemeError
5186+
5187+
with pytest.raises(AuthSchemeError, match="Could not determine"):
5188+
get_authenticator(bypass_url, "test-key")
5189+
5190+
51685191
def test_get_authenticator_sets_http_timeout_on_iam():
51695192
from langflow.services.adapters.deployment.watsonx_orchestrate.client import get_authenticator
51705193

0 commit comments

Comments
 (0)