Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package azure-cli-core for openSUSE:Factory checked in at 2026-09-07 11:29:51 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/azure-cli-core (Old) and /work/SRC/openSUSE:Factory/.azure-cli-core.new.1265 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "azure-cli-core" Mon Sep 7 11:29:51 2026 rev:100 rq:1375727 version:2.90.0 Changes: -------- --- /work/SRC/openSUSE:Factory/azure-cli-core/azure-cli-core.changes 2026-08-21 16:58:09.559261646 +0200 +++ /work/SRC/openSUSE:Factory/.azure-cli-core.new.1265/azure-cli-core.changes 2026-09-07 11:31:35.045738142 +0200 @@ -1,0 +2,8 @@ +Tue Sep 1 09:24:15 UTC 2026 - John Paul Adrian Glaubitz <[email protected]> + +- New upstream release + + Version 2.90.0 + + For detailed information about changes see the + HISTORY.rst file provided with this package + +------------------------------------------------------------------- Old: ---- azure_cli_core-2.89.1.tar.gz New: ---- azure_cli_core-2.90.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ azure-cli-core.spec ++++++ --- /var/tmp/diff_new_pack.mS9KQT/_old 2026-09-07 11:31:35.975770742 +0200 +++ /var/tmp/diff_new_pack.mS9KQT/_new 2026-09-07 11:31:35.977770812 +0200 @@ -24,7 +24,7 @@ %global _sitelibdir %{%{pythons}_sitelib} Name: azure-cli-core -Version: 2.89.1 +Version: 2.90.0 Release: 0 Summary: Microsoft Azure CLI Core Module License: MIT ++++++ azure_cli_core-2.89.1.tar.gz -> azure_cli_core-2.90.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.89.1/HISTORY.rst new/azure_cli_core-2.90.0/HISTORY.rst --- old/azure_cli_core-2.89.1/HISTORY.rst 2026-08-10 03:11:59.000000000 +0200 +++ new/azure_cli_core-2.90.0/HISTORY.rst 2026-08-26 03:18:26.000000000 +0200 @@ -3,6 +3,10 @@ Release History =============== +2.90.0 +++++++ +* Minor fixes + 2.89.1 ++++++ * Allow SSH certificate flow in Cloud Shell (#33860) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.89.1/PKG-INFO new/azure_cli_core-2.90.0/PKG-INFO --- old/azure_cli_core-2.89.1/PKG-INFO 2026-08-10 03:12:55.453722500 +0200 +++ new/azure_cli_core-2.90.0/PKG-INFO 2026-08-26 03:19:17.575173000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: azure-cli-core -Version: 2.89.1 +Version: 2.90.0 Summary: Microsoft Azure Command-Line Tools Core Module Home-page: https://github.com/Azure/azure-cli Author: Microsoft Corporation diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.89.1/azure/cli/core/__init__.py new/azure_cli_core-2.90.0/azure/cli/core/__init__.py --- old/azure_cli_core-2.89.1/azure/cli/core/__init__.py 2026-08-10 03:11:59.000000000 +0200 +++ new/azure_cli_core-2.90.0/azure/cli/core/__init__.py 2026-08-26 03:18:26.000000000 +0200 @@ -4,7 +4,7 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=line-too-long -__version__ = "2.89.1" +__version__ = "2.90.0" import os import sys diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.89.1/azure/cli/core/aaz/_arg_fmt.py new/azure_cli_core-2.90.0/azure/cli/core/aaz/_arg_fmt.py --- old/azure_cli_core-2.89.1/azure/cli/core/aaz/_arg_fmt.py 2026-08-10 03:11:59.000000000 +0200 +++ new/azure_cli_core-2.90.0/azure/cli/core/aaz/_arg_fmt.py 2026-08-26 03:18:26.000000000 +0200 @@ -767,7 +767,9 @@ # `next_link` is a URL that the next page request is sent to with an Azure access token. # Validate it shares the same origin as a trusted endpoint of the active cloud to prevent # the token from being sent to an attacker-controlled host such as - # `https://management.azure.com.attacker`. + # `https://management.azure.com.attacker`. URLs that are not normalized, such as + # `https://management.azure.com//attacker.example/leak`, are rejected as well, since they + # may be resolved to a different host by a downstream parser, proxy or server. if next_link is not None: from azure.cli.core.util import is_trusted_cloud_endpoint if not is_trusted_cloud_endpoint(next_link, ctx.cli_ctx): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.89.1/azure/cli/core/commandIndex.latest.json new/azure_cli_core-2.90.0/azure/cli/core/commandIndex.latest.json --- old/azure_cli_core-2.89.1/azure/cli/core/commandIndex.latest.json 2026-08-10 03:11:59.000000000 +0200 +++ new/azure_cli_core-2.90.0/azure/cli/core/commandIndex.latest.json 2026-08-26 03:18:26.000000000 +0200 @@ -1,5 +1,5 @@ { - "version": "2.89.1", + "version": "2.90.0", "cloudProfile": "latest", "commandIndex": { "account": [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.89.1/azure/cli/core/extension/operations.py new/azure_cli_core-2.90.0/azure/cli/core/extension/operations.py --- old/azure_cli_core-2.89.1/azure/cli/core/extension/operations.py 2026-08-10 03:11:59.000000000 +0200 +++ new/azure_cli_core-2.90.0/azure/cli/core/extension/operations.py 2026-08-26 03:18:26.000000000 +0200 @@ -180,7 +180,7 @@ return extension_name -def _install_deps_for_psycopg2(): # pylint: disable=too-many-statements +def _install_deps_for_psycopg2(): # pylint: disable=too-many-statements, too-many-branches # If we are in Cloud Shell, dependencies should have already been installed. from azure.cli.core.util import in_cloud_console if in_cloud_console(): @@ -215,8 +215,17 @@ '/usr/local/opt/openssl/lib/' ]) elif system == 'Linux': - distname, _ = get_linux_distro() - distname = distname.lower().strip() + distname, distversion = get_linux_distro() + distname = (distname or '').lower().strip() + distversion = (distversion or '').strip() + # Extract major version from VERSION_ID (e.g., "4", "4.0", "4.0.1" all map to major=4) + azl_major_version = None + if 'azure linux' in distname and distversion: + try: + azl_major_version = int(distversion.partition('.')[0]) + except ValueError: + pass + is_azure_linux_4 = azl_major_version == 4 if installer == 'DEB' or any(x in distname for x in ['ubuntu', 'debian']): exit_code = subprocess.call(['dpkg', '-s', 'gcc', 'libpq-dev', 'python3-dev'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if exit_code != 0: @@ -230,7 +239,7 @@ if exit_code == 0: logger.debug("Install dependencies with '%s'", " ".join(apt_install_cmd)) subprocess.call(apt_install_cmd, True) - elif installer == 'RPM' or any(x in distname for x in ['centos', 'rhel', 'red hat', 'fedora', 'opensuse', 'suse', 'sles']): + elif installer == 'RPM' or any(x in distname for x in ['centos', 'rhel', 'red hat', 'fedora', 'opensuse', 'suse', 'sles']) or is_azure_linux_4: if any(x in distname for x in ['centos', 'rhel', 'red hat', 'fedora']): yum_install_cmd = 'yum install -y gcc postgresql-devel python3-devel'.split() if os.geteuid() != 0: # pylint: disable=no-member @@ -249,6 +258,29 @@ if exit_code == 0: logger.debug("Install dependencies with '%s'", " ".join(zypper_install_cmd)) subprocess.call(zypper_install_cmd) + elif is_azure_linux_4: + from shutil import which + if which('tdnf') is None: + raise CLIError( + 'tdnf package manager not found. This extension requires Azure Linux 4 with tdnf ' + 'to install required system dependencies: gcc, libpq-devel, python3-devel, binutils, ' + 'glibc-devel, kernel-headers.' + ) + rpm_packages = ['gcc', 'libpq-devel', 'python3-devel', 'binutils', 'glibc-devel', 'kernel-headers'] + rpm_install_cmd = ['tdnf', 'install', '-y'] + rpm_packages + if os.geteuid() != 0: # pylint: disable=no-member + rpm_install_cmd.insert(0, 'sudo') + logger.debug("Install dependencies with '%s'", " ".join(rpm_install_cmd)) + logger.warning( + 'This extension depends on %s and will now be installed.', + ' '.join(rpm_packages) + ) + exit_code = subprocess.call(rpm_install_cmd) + if exit_code != 0: + raise CLIError( + 'Failed to install required system dependencies for psycopg2: {}.' + .format(' '.join(rpm_packages)) + ) elif installer == 'DOCKER' or any(x in distname for x in ['alpine linux']): apk_install_cmd = 'apk add --no-cache libpq-dev'.split() logger.debug("Install dependencies with '%s'", " ".join(apk_install_cmd)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.89.1/azure/cli/core/helpIndex.latest.json new/azure_cli_core-2.90.0/azure/cli/core/helpIndex.latest.json --- old/azure_cli_core-2.89.1/azure/cli/core/helpIndex.latest.json 2026-08-10 03:11:59.000000000 +0200 +++ new/azure_cli_core-2.90.0/azure/cli/core/helpIndex.latest.json 2026-08-26 03:18:26.000000000 +0200 @@ -1,5 +1,5 @@ { - "version": "2.89.1", + "version": "2.90.0", "cloudProfile": "latest", "helpIndex": { "groups": { @@ -152,7 +152,7 @@ "tags": "" }, "eventhubs": { - "summary": "Eventhubs.", + "summary": "Manage Event Hub.", "tags": "" }, "extension": { @@ -292,7 +292,7 @@ "tags": "" }, "servicebus": { - "summary": "Servicebus.", + "summary": "Manage Service Bus.", "tags": "" }, "sf": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.89.1/azure/cli/core/util.py new/azure_cli_core-2.90.0/azure/cli/core/util.py --- old/azure_cli_core-2.89.1/azure/cli/core/util.py 2026-08-10 03:11:59.000000000 +0200 +++ new/azure_cli_core-2.90.0/azure/cli/core/util.py 2026-08-26 03:18:26.000000000 +0200 @@ -1028,7 +1028,13 @@ # default to Azure Resource Manager. # https://management.azure.com + /subscriptions/xxx/resourcegroups/xxx?api-version=2019-07-01 if '://' not in url: - url = endpoints.resource_manager.rstrip('/') + url + # lstrip('/') prevents a protocol-relative path like '//attacker.example/leak' from + # producing 'https://management.azure.com//attacker.example/leak'. + url = endpoints.resource_manager.rstrip('/') + '/' + url.lstrip('/') + + # Normalize before validating and before sending, so that the URL whose origin is validated is + # exactly the URL the request is sent to. + url = normalize_url(url) # Replace common tokens with real values. It is for smooth experience if users copy and paste the url from # Azure Rest API doc @@ -1075,6 +1081,13 @@ req = Request(method=method, url=url, headers=headers, params=uri_parameters, data=body) prepped = s.prepare_request(req) + # Final defense: make sure requests didn't resolve the URL to a different origin than the one + # whose origin was validated above before an access token was attached. + if 'Authorization' in prepped.headers and not is_same_origin(prepped.url, url): + from .azclierror import InvalidArgumentValueError + raise InvalidArgumentValueError( + "The request URL '{}' doesn't match the validated URL '{}'.".format(prepped.url, url)) + # Merge environment settings into session settings = s.merge_environment_settings(prepped.url, {}, None, not should_disable_connection_verify(), None) _log_request(prepped) @@ -1202,6 +1215,92 @@ return req.read() +def _remove_dot_segments(path): + """Remove ``.`` and ``..`` segments from a URL path (RFC 3986 section 5.2.4) and collapse + empty segments, so that ``/a/b/../c`` becomes ``/a/c`` and ``//attacker.example/leak`` + becomes ``/attacker.example/leak``. + """ + leading_slash = path.startswith('/') + trailing_slash = path.endswith('/') + segments = [] + for segment in path.split('/'): + # Empty segments are dropped so that a path can never be re-interpreted as an authority + # (protocol-relative URL) by a downstream parser, proxy or gateway. + if segment in ('', '.'): + continue + if segment == '..': + if segments: + segments.pop() + continue + segments.append(segment) + new_path = '/'.join(segments) + if leading_slash: + new_path = '/' + new_path + if trailing_slash and not new_path.endswith('/'): + new_path += '/' + return new_path + + +def normalize_url(url): + """Normalize a URL so that it is parsed the same way by the CLI and by any downstream + consumer (requests/urllib3, proxies, gateways, servers). + + Validating a URL that is not normalized is unsafe, because the validator and the consumer + may disagree on which host the request is actually sent to. This function: + + - removes control characters, which are silently stripped by many URL parsers, and trims + leading/trailing whitespace; + - lowercases the scheme and the host; + - converts backslashes in the authority to ``/`` (WHATWG URL parsers treat ``\\`` as ``/``), + moving anything after them into the path; + - removes ``.``/``..`` segments and collapses empty segments in the path, so that + ``https://management.azure.com//attacker.example/leak`` is normalized to + ``https://management.azure.com/attacker.example/leak``. + + :param url: The URL to normalize. + :return: The normalized URL, or the input unchanged if it is not a parsable string. + :rtype: str + """ + from urllib.parse import urlsplit, urlunsplit + + if not isinstance(url, str): + return url + + # Strip C0/DEL control characters. Browsers and urllib strip some of them (\t, \r, \n), so a + # validator must not consider them part of the host. + # Note: the space character (U+0020) is *not* removed from the middle of the URL, as it is a + # meaningful character that downstream consumers percent-encode as '%20' rather than drop. + # Only leading and trailing whitespace is trimmed. + cleaned = ''.join(c for c in url if ord(c) >= 0x20 and ord(c) != 0x7f).strip() + + try: + parts = urlsplit(cleaned) + except ValueError: + return cleaned + + scheme = parts.scheme.lower() + netloc = parts.netloc + path = parts.path + + if netloc: + # `urlsplit` doesn't treat '\' as a delimiter, but WHATWG parsers do. Anything after it + # belongs to the path, not to the authority. + netloc = netloc.replace('\\', '/') + if '/' in netloc: + netloc, _, rest = netloc.partition('/') + path = '/' + rest + path + # The host is case-insensitive, but userinfo isn't, so only lowercase the host part. + userinfo, sep, hostport = netloc.rpartition('@') + netloc = userinfo + sep + hostport.lower() + + if path: + path = _remove_dot_segments(path) + elif netloc: + path = '/' + + return urlunsplit((scheme, netloc, path, parts.query, parts.fragment)) + + def is_same_origin(url, endpoint): """Check whether ``url`` and ``endpoint`` share the same origin (scheme + host + port). @@ -1211,6 +1310,9 @@ URL points to a trusted endpoint before sensitive data (e.g., an Azure access token) is sent to it. + The URLs are normalized with :func:`normalize_url` first, so that tricks relying on control + characters or backslashes in the authority can't hide the real host. + :param url: The URL to validate, e.g., ``https://management.azure.com/subscriptions/...``. :param endpoint: The trusted endpoint to validate against, e.g., ``https://management.azure.com/``. :return: ``True`` if both share the same origin, otherwise ``False``. @@ -1222,8 +1324,8 @@ return False try: - url_parts = urlparse(url) - endpoint_parts = urlparse(endpoint) + url_parts = urlparse(normalize_url(url)) + endpoint_parts = urlparse(normalize_url(endpoint)) except (TypeError, ValueError): return False @@ -1280,11 +1382,19 @@ def is_trusted_cloud_endpoint(url, cli_ctx): """Check whether ``url`` shares the same origin as any endpoint of the active cloud. + The URL must already be in normalized form (see :func:`normalize_url`). A URL that is not + normalized is rejected, because the CLI and the downstream consumer (requests, a proxy or the + server) may not resolve it to the same host, e.g. + ``https://management.azure.com//attacker.example/leak``, whose path can be re-interpreted as + a protocol-relative URL. + :param url: The URL to validate, e.g., ``https://management.azure.com/subscriptions/...``. :param cli_ctx: The CLI context whose active cloud's endpoints are treated as trusted. :return: ``True`` if ``url`` shares the same origin as any cloud endpoint, otherwise ``False``. :rtype: bool """ + if not isinstance(url, str) or normalize_url(url) != url: + return False return match_cloud_endpoint(url, cli_ctx) is not None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.89.1/azure_cli_core.egg-info/PKG-INFO new/azure_cli_core-2.90.0/azure_cli_core.egg-info/PKG-INFO --- old/azure_cli_core-2.89.1/azure_cli_core.egg-info/PKG-INFO 2026-08-10 03:12:55.000000000 +0200 +++ new/azure_cli_core-2.90.0/azure_cli_core.egg-info/PKG-INFO 2026-08-26 03:19:17.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: azure-cli-core -Version: 2.89.1 +Version: 2.90.0 Summary: Microsoft Azure Command-Line Tools Core Module Home-page: https://github.com/Azure/azure-cli Author: Microsoft Corporation diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.89.1/setup.py new/azure_cli_core-2.90.0/setup.py --- old/azure_cli_core-2.89.1/setup.py 2026-08-10 03:11:59.000000000 +0200 +++ new/azure_cli_core-2.90.0/setup.py 2026-08-26 03:18:26.000000000 +0200 @@ -8,7 +8,7 @@ from codecs import open from setuptools import setup, find_packages -VERSION = "2.89.1" +VERSION = "2.90.0" # If we have source, validate that our version numbers match # This should prevent uploading releases with mismatched versions.
