fix: enforce trusted-endpoint validation for all authentication methods (CWE-918) - #642
fix: enforce trusted-endpoint validation for all authentication methods (CWE-918)#642tanmaya-panda1 wants to merge 1 commit into
Conversation
…ds (CWE-918) validate_endpoint() only invoked well_known_kusto_endpoints.validate_trusted_endpoint() when the token provider was a CloudInfoTokenProvider. BasicTokenProvider and CallbackTokenProvider derive from TokenProviderBase directly, so the flows built by with_aad_user_token_authentication, with_aad_application_token_authentication, with_token_provider and with_async_token_provider skipped host validation entirely while still sending 'Authorization: Bearer <token>' to whatever host the connection string named. The Java SDK (ClientImpl.validateEndpointAsync) validates unconditionally, so Python was the outlier. Removing the gate alone would have introduced a second problem: resolving the login endpoint calls CloudSettings.get_cloud_info_for_cluster, which issues a GET to the cluster itself. Validating after that call means an untrusted connection string still drives a request to an arbitrary host, leaving an SSRF primitive, and it would newly require the metadata endpoint on hosts that are trusted explicitly via add_trusted_hosts. The login endpoint is therefore now resolved lazily, and only once the hostname is known to appear in at least one cloud's allow list. Hosts that no login endpoint could ever make trusted are rejected without any network call, and hosts trusted via add_trusted_hosts or an override policy no longer need cloud metadata at all. Passing a plain string is still supported. Known limitation, pre-existing and unchanged in scope: the async client calls the synchronous validate_endpoint, so metadata resolution still blocks the event loop for allow-listed hosts. That needs an async CloudSettings path and is left as a follow-up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 41fe4674-b54d-43ae-9bd2-6769c81623e4
Test Results 5 files ± 0 5 suites ±0 7m 9s ⏱️ - 4m 47s For more details on these failures, see this check. Results for commit 43e8594. ± Comparison against base commit 5a8a712. |
|
CI note for reviewers: the 5 The only failing tests are Root cause is Azure-side data drift, not code. Application Insights has since gained a Two further points confirming independence from this change:
The fix belongs in a separate PR that makes the assertion tolerant of new tables rather than requiring an exact ordered match. Happy to open that separately if useful. |
Remediation for IcM 842305145 (Glasswing Mythos - July, follow-on wave to Twin Flames).
Issue
_KustoClientBase.validate_endpoint()only invokedwell_known_kusto_endpoints.validate_trusted_endpoint()when the token provider was aCloudInfoTokenProvider; otherwise it simply set_endpoint_validated = Trueand returned.BasicTokenProviderandCallbackTokenProviderderive fromTokenProviderBasedirectly, not fromCloudInfoTokenProvider. As a result these flows skipped trusted-host validation entirely while still sendingAuthorization: Bearer <token>to whatever host the connection string named:with_aad_user_token_authenticationwith_aad_application_token_authenticationwith_token_providerwith_async_token_providerThe Java SDK validates unconditionally for every auth type (
ClientImpl.validateEndpointAsync), so Python was the outlier here rather than this being an intentional design choice.Why the obvious fix was not enough
Simply deleting the
isinstancegate would have introduced a second problem. Resolving the login endpoint callsCloudSettings.get_cloud_info_for_cluster, which issues a GET to the cluster host itself. Validating only after that call means an untrusted connection string still drives a request to an arbitrary host, which leaves an SSRF primitive in place even though the bearer token is no longer disclosed. It would also have newly required the/v1/rest/auth/metadataendpoint on hosts that callers had explicitly trusted throughadd_trusted_hosts, breaking custom proxies that do not implement it.Fix
validate_trusted_endpoint/validate_hostname_is_trustednow accept the login endpoint either as a plain string (unchanged behaviour for existing callers) or as a callable that is resolved lazily. The callable is only invoked once the hostname is already known to appear in at least one cloud allow list.Consequences:
add_trusted_hostsor an override policy, and local addresses, are resolved without any cloud metadata lookup.client_base.validate_endpoint()passes the lookup as a lambda and no longer gates on provider type.Tests
New
azure-kusto-data/tests/test_endpoint_validation.pycovers all four previously-bypassing auth methods:Validation
master: 133 passed, 11 failed, 3 skipped, 42 errors.ruff format --checkclean across all 51 files inazure-kusto-data.azure-kusto-ingestneeds no separate change: bothingest_client.pyandstreaming_ingest_client.pydelegate to the data-layerKustoClientand inherit this validation.Known limitation, pre-existing and unchanged in scope
The async client (
aio/client.py:174) calls the synchronousvalidate_endpoint, so metadata resolution still blocks the event loop for allow-listed hosts, and the underlyingrequests.gethas no explicit timeout. This already affected everyCloudInfo-based provider before this change. Fixing it properly requires an asyncCloudSettingspath and is better handled as a separate PR.