From 620fa0c1050a291bc292eebe5f3ade5d28781eac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:00:30 +0000 Subject: [PATCH 01/11] Initial plan From 5346a46a92a64cb423ebcfef755827a15d6a221f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:10:29 +0000 Subject: [PATCH 02/11] Fix WSL browser interop detection Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- .../azure/cli/core/auth/identity.py | 14 +-- .../cli/core/auth/tests/test_identity.py | 17 ++++ .../azure/cli/core/tests/test_util.py | 31 ++++++- src/azure-cli-core/azure/cli/core/util.py | 85 +++++++++++++++++-- 4 files changed, 130 insertions(+), 17 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/auth/identity.py b/src/azure-cli-core/azure/cli/core/auth/identity.py index 91629e89441..2bff03c0179 100644 --- a/src/azure-cli-core/azure/cli/core/auth/identity.py +++ b/src/azure-cli-core/azure/cli/core/auth/identity.py @@ -163,12 +163,14 @@ def _prompt_launching_ui(ui=None, **_): # For AAD, use port 0 to let the system choose arbitrary unused ephemeral port to avoid port collision # on port 8400 from the old design. However, ADFS only allows port 8400. - result = self._msal_app.acquire_token_interactive( - scopes, prompt='select_account', port=8400 if self._is_adfs else None, - success_template=success_template, error_template=error_template, - parent_window_handle=self._msal_app.CONSOLE_WINDOW_HANDLE, on_before_launching_ui=_prompt_launching_ui, - enable_msa_passthrough=True, - claims_challenge=claims_challenge) + from azure.cli.core.util import wsl_browser_open + with wsl_browser_open(): + result = self._msal_app.acquire_token_interactive( + scopes, prompt='select_account', port=8400 if self._is_adfs else None, + success_template=success_template, error_template=error_template, + parent_window_handle=self._msal_app.CONSOLE_WINDOW_HANDLE, on_before_launching_ui=_prompt_launching_ui, + enable_msa_passthrough=True, + claims_challenge=claims_challenge) return check_result(result) def login_with_device_code(self, scopes, claims_challenge=None): diff --git a/src/azure-cli-core/azure/cli/core/auth/tests/test_identity.py b/src/azure-cli-core/azure/cli/core/auth/tests/test_identity.py index 993039faca3..b29a570c92c 100644 --- a/src/azure-cli-core/azure/cli/core/auth/tests/test_identity.py +++ b/src/azure-cli-core/azure/cli/core/auth/tests/test_identity.py @@ -45,6 +45,23 @@ class TestIdentity(unittest.TestCase): + @mock.patch("azure.cli.core.auth.identity.check_result", return_value={"username": "user1"}) + @mock.patch("azure.cli.core.auth.util.read_response_templates", return_value=("success", "error")) + @mock.patch("azure.cli.core.util.wsl_browser_open") + def test_login_with_auth_code_uses_wsl_browser_open(self, wsl_browser_open_mock, _, check_result_mock): + identity = Identity.__new__(Identity) + identity._is_adfs = False + identity._msal_app_instance = mock.MagicMock() + identity._msal_app_instance.CONSOLE_WINDOW_HANDLE = "console" + identity._msal_app_instance.acquire_token_interactive.return_value = {"access_token": "token"} + + result = identity.login_with_auth_code(["scope"]) + + self.assertEqual(result, {"username": "user1"}) + wsl_browser_open_mock.assert_called_once() + identity._msal_app_instance.acquire_token_interactive.assert_called_once() + check_result_mock.assert_called_once_with({"access_token": "token"}) + @mock.patch("azure.cli.core.auth.identity.ServicePrincipalStore.save_entry") @mock.patch("msal.application.ConfidentialClientApplication.acquire_token_for_client") @mock.patch("msal.application.ConfidentialClientApplication.__init__", return_value=None) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_util.py b/src/azure-cli-core/azure/cli/core/tests/test_util.py index 47bce9832fa..457071c6acd 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_util.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_util.py @@ -17,7 +17,8 @@ (get_file_json, truncate_text, shell_safe_json_parse, b64_to_hex, hash_string, random_string, open_page_in_browser, can_launch_browser, handle_exception, ConfiguredDefaultSetter, send_raw_request, should_disable_connection_verify, parse_proxy_resource_id, get_az_user_agent, get_az_rest_user_agent, - _get_parent_proc_name, is_wsl, run_cmd, run_az_cmd, roughly_parse_command, is_same_origin) + _get_parent_proc_name, is_wsl, run_cmd, run_az_cmd, roughly_parse_command, is_same_origin, + _is_wsl_interop_enabled, wsl_browser_open) from azure.cli.core.mock import DummyCli @@ -179,8 +180,9 @@ def test_open_page_in_browser(self, subprocess_open_mock, webbrowser_open_mock): @mock.patch('shutil.which', autospec=True) @mock.patch('azure.cli.core.util._get_platform_info', autospec=True) + @mock.patch('azure.cli.core.util._is_wsl_interop_enabled', autospec=True, return_value=True) @mock.patch('webbrowser.get', autospec=True) - def test_can_launch_browser(self, webbrowser_get_mock, get_platform_mock, which_mock): + def test_can_launch_browser(self, webbrowser_get_mock, _, get_platform_mock, which_mock): import webbrowser # Windows is always fine @@ -207,6 +209,7 @@ def test_can_launch_browser(self, webbrowser_get_mock, get_platform_mock, which_ get_platform_mock.return_value = ('linux', '5.10.16.3-microsoft-standard-WSL2') browser_mock = mock.MagicMock() browser_mock.name = 'www-browser' + webbrowser_get_mock.side_effect = None webbrowser_get_mock.return_value = browser_mock assert can_launch_browser() @@ -223,6 +226,30 @@ def test_can_launch_browser(self, webbrowser_get_mock, get_platform_mock, which_ which_mock.return_value = False assert not can_launch_browser() + @mock.patch('azure.cli.core.util._get_platform_info', autospec=True, + return_value=('linux', '5.10.16.3-microsoft-standard-WSL2')) + @mock.patch('os.listdir', autospec=True, return_value=['status', 'WSLInterop-late']) + def test_is_wsl_interop_enabled_with_late_entry(self, listdir_mock, _): + m = mock.mock_open(read_data='enabled\ninterpreter /init\n') + with mock.patch('builtins.open', m): + assert _is_wsl_interop_enabled() + + listdir_mock.assert_called_once_with('/proc/sys/fs/binfmt_misc') + m.assert_called_once_with('/proc/sys/fs/binfmt_misc/WSLInterop-late', 'r') + + @mock.patch('azure.cli.core.util._open_url_in_wsl_browser', autospec=True, return_value=True) + @mock.patch('azure.cli.core.util._get_platform_info', autospec=True, + return_value=('linux', '5.10.16.3-microsoft-standard-WSL2')) + def test_wsl_browser_open_uses_wsl_browser(self, _, open_url_mock): + import webbrowser + + original_open = webbrowser.open + with wsl_browser_open(): + assert webbrowser.open('https://login.example.com') + + open_url_mock.assert_called_once_with('https://login.example.com') + self.assertEqual(webbrowser.open, original_open) + def test_configured_default_setter(self): config = mock.MagicMock() config.use_local_config = None diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index 67c7650f5fa..eb6bfa538de 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -799,14 +799,8 @@ def open_page_in_browser(url): platform_name, _ = _get_platform_info() if is_wsl(): # windows 10 linux subsystem - try: - # https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe - # Ampersand (&) should be quoted - safe_url = url.replace("'", "''") - return subprocess.Popen( - ['powershell.exe', '-NoProfile', '-Command', f"Start-Process '{safe_url}'"]).wait() - except OSError: # WSL might be too old # FileNotFoundError introduced in Python 3 - pass + if _open_url_in_wsl_browser(url): + return True elif platform_name == 'darwin': # handle 2 things: # a. On OSX sierra, 'python -m webbrowser -t ' emits out "execution error: doesn't @@ -833,6 +827,78 @@ def is_wsl(): return platform_name == 'linux' and 'microsoft' in release +def _is_wsl_interop_enabled(): + if not is_wsl(): + return False + + try: + binfmt_entries = os.listdir('/proc/sys/fs/binfmt_misc') + except OSError: + return bool(os.environ.get('WSL_INTEROP')) + + for entry in binfmt_entries: + if not entry.startswith('WSLInterop'): + continue + try: + with open(os.path.join('/proc/sys/fs/binfmt_misc', entry), 'r') as f: + if f.readline().strip() == 'enabled': + return True + except OSError: + continue + + return bool(os.environ.get('WSL_INTEROP')) + + +def _get_wsl_browser_commands(url): + safe_url = url.replace("'", "''") + return [ + (['/mnt/c/Windows/explorer.exe', url], (0, 1)), + (['explorer.exe', url], (0, 1)), + (['/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe', + '-NoProfile', '-Command', f"Start-Process '{safe_url}'"], (0,)), + (['powershell.exe', '-NoProfile', '-Command', f"Start-Process '{safe_url}'"], (0,)) + ] + + +def _open_url_in_wsl_browser(url): + if not _is_wsl_interop_enabled(): + return False + + import subprocess + for cmd, success_codes in _get_wsl_browser_commands(url): + try: + exit_code = subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + if exit_code in success_codes: + return True + except OSError: + continue + return False + + +class _WslBrowserOpen: + def __enter__(self): + self._original_open = None + if not is_wsl(): + return + + import webbrowser + self._original_open = webbrowser.open + + def _open(url, *args, **kwargs): + return _open_url_in_wsl_browser(url) or self._original_open(url, *args, **kwargs) + + webbrowser.open = _open + + def __exit__(self, *args): + if self._original_open: + import webbrowser + webbrowser.open = self._original_open + + +def wsl_browser_open(): + return _WslBrowserOpen() + + def is_windows(): platform_name, _ = _get_platform_info() return platform_name == 'windows' @@ -863,7 +929,8 @@ def can_launch_browser(): # Docker container running on WSL 2 also shows WSL, but it can't launch a browser. # If powershell.exe is on PATH, it can be called to launch a browser. import shutil - if shutil.which("powershell.exe"): + if _is_wsl_interop_enabled() and ( + shutil.which("powershell.exe") or os.path.exists('/mnt/c/Windows/explorer.exe')): return True return False From 3a7b37fb306a9be165bc48947fb2f11ab5237442 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:11:35 +0000 Subject: [PATCH 03/11] Address WSL browser review feedback Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- .../azure/cli/core/tests/test_util.py | 8 +++++++- src/azure-cli-core/azure/cli/core/util.py | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_util.py b/src/azure-cli-core/azure/cli/core/tests/test_util.py index 457071c6acd..999b255884e 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_util.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_util.py @@ -18,7 +18,7 @@ open_page_in_browser, can_launch_browser, handle_exception, ConfiguredDefaultSetter, send_raw_request, should_disable_connection_verify, parse_proxy_resource_id, get_az_user_agent, get_az_rest_user_agent, _get_parent_proc_name, is_wsl, run_cmd, run_az_cmd, roughly_parse_command, is_same_origin, - _is_wsl_interop_enabled, wsl_browser_open) + _is_wsl_interop_enabled, _open_url_in_wsl_browser, wsl_browser_open) from azure.cli.core.mock import DummyCli @@ -250,6 +250,12 @@ def test_wsl_browser_open_uses_wsl_browser(self, _, open_url_mock): open_url_mock.assert_called_once_with('https://login.example.com') self.assertEqual(webbrowser.open, original_open) + @mock.patch('subprocess.call', autospec=True) + @mock.patch('azure.cli.core.util._is_wsl_interop_enabled', autospec=True, return_value=True) + def test_open_url_in_wsl_browser_rejects_non_http_url(self, _, subprocess_call_mock): + assert not _open_url_in_wsl_browser('/C:/Windows/System32/calc.exe') + subprocess_call_mock.assert_not_called() + def test_configured_default_setter(self): config = mock.MagicMock() config.use_local_config = None diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index eb6bfa538de..2e94ca3d1fe 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -860,8 +860,19 @@ def _get_wsl_browser_commands(url): ] +def _is_safe_browser_url(url): + from urllib.parse import urlparse + + try: + parsed_url = urlparse(url) + except ValueError: + return False + + return parsed_url.scheme in ('http', 'https') and not any(c in url for c in '\n\r\t') + + def _open_url_in_wsl_browser(url): - if not _is_wsl_interop_enabled(): + if not _is_safe_browser_url(url) or not _is_wsl_interop_enabled(): return False import subprocess @@ -878,6 +889,7 @@ def _open_url_in_wsl_browser(url): class _WslBrowserOpen: def __enter__(self): self._original_open = None + self._patch_applied = False if not is_wsl(): return @@ -888,9 +900,10 @@ def _open(url, *args, **kwargs): return _open_url_in_wsl_browser(url) or self._original_open(url, *args, **kwargs) webbrowser.open = _open + self._patch_applied = True def __exit__(self, *args): - if self._original_open: + if self._patch_applied: import webbrowser webbrowser.open = self._original_open From 87665fc818f05a0fe1512f39b21a523ea6ac212c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:13:21 +0000 Subject: [PATCH 04/11] Refine WSL browser wrapper Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- src/azure-cli-core/azure/cli/core/auth/identity.py | 3 ++- .../azure/cli/core/auth/tests/test_identity.py | 2 +- src/azure-cli-core/azure/cli/core/util.py | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/auth/identity.py b/src/azure-cli-core/azure/cli/core/auth/identity.py index 2bff03c0179..432e38d142a 100644 --- a/src/azure-cli-core/azure/cli/core/auth/identity.py +++ b/src/azure-cli-core/azure/cli/core/auth/identity.py @@ -13,6 +13,8 @@ from knack.util import CLIError from msal import PublicClientApplication, ConfidentialClientApplication +from azure.cli.core.util import wsl_browser_open + from .constants import AZURE_CLI_CLIENT_ID from .msal_credentials import UserCredential, ServicePrincipalCredential from .persistence import load_persisted_token_cache, file_extensions, load_secret_store @@ -163,7 +165,6 @@ def _prompt_launching_ui(ui=None, **_): # For AAD, use port 0 to let the system choose arbitrary unused ephemeral port to avoid port collision # on port 8400 from the old design. However, ADFS only allows port 8400. - from azure.cli.core.util import wsl_browser_open with wsl_browser_open(): result = self._msal_app.acquire_token_interactive( scopes, prompt='select_account', port=8400 if self._is_adfs else None, diff --git a/src/azure-cli-core/azure/cli/core/auth/tests/test_identity.py b/src/azure-cli-core/azure/cli/core/auth/tests/test_identity.py index b29a570c92c..f581f11e2d3 100644 --- a/src/azure-cli-core/azure/cli/core/auth/tests/test_identity.py +++ b/src/azure-cli-core/azure/cli/core/auth/tests/test_identity.py @@ -47,7 +47,7 @@ class TestIdentity(unittest.TestCase): @mock.patch("azure.cli.core.auth.identity.check_result", return_value={"username": "user1"}) @mock.patch("azure.cli.core.auth.util.read_response_templates", return_value=("success", "error")) - @mock.patch("azure.cli.core.util.wsl_browser_open") + @mock.patch("azure.cli.core.auth.identity.wsl_browser_open") def test_login_with_auth_code_uses_wsl_browser_open(self, wsl_browser_open_mock, _, check_result_mock): identity = Identity.__new__(Identity) identity._is_adfs = False diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index 2e94ca3d1fe..469c1073acc 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -852,8 +852,6 @@ def _is_wsl_interop_enabled(): def _get_wsl_browser_commands(url): safe_url = url.replace("'", "''") return [ - (['/mnt/c/Windows/explorer.exe', url], (0, 1)), - (['explorer.exe', url], (0, 1)), (['/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe', '-NoProfile', '-Command', f"Start-Process '{safe_url}'"], (0,)), (['powershell.exe', '-NoProfile', '-Command', f"Start-Process '{safe_url}'"], (0,)) @@ -891,7 +889,7 @@ def __enter__(self): self._original_open = None self._patch_applied = False if not is_wsl(): - return + return self import webbrowser self._original_open = webbrowser.open @@ -901,6 +899,7 @@ def _open(url, *args, **kwargs): webbrowser.open = _open self._patch_applied = True + return self def __exit__(self, *args): if self._patch_applied: @@ -943,7 +942,8 @@ def can_launch_browser(): # If powershell.exe is on PATH, it can be called to launch a browser. import shutil if _is_wsl_interop_enabled() and ( - shutil.which("powershell.exe") or os.path.exists('/mnt/c/Windows/explorer.exe')): + shutil.which("powershell.exe") or + os.path.exists('/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe')): return True return False From e8e170715bf813d7f6d893bcb6e456f65c1c255b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:14:04 +0000 Subject: [PATCH 05/11] Clarify WSL auth test coverage Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- .../azure/cli/core/auth/tests/test_identity.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/auth/tests/test_identity.py b/src/azure-cli-core/azure/cli/core/auth/tests/test_identity.py index f581f11e2d3..11db78bb606 100644 --- a/src/azure-cli-core/azure/cli/core/auth/tests/test_identity.py +++ b/src/azure-cli-core/azure/cli/core/auth/tests/test_identity.py @@ -51,15 +51,16 @@ class TestIdentity(unittest.TestCase): def test_login_with_auth_code_uses_wsl_browser_open(self, wsl_browser_open_mock, _, check_result_mock): identity = Identity.__new__(Identity) identity._is_adfs = False - identity._msal_app_instance = mock.MagicMock() - identity._msal_app_instance.CONSOLE_WINDOW_HANDLE = "console" - identity._msal_app_instance.acquire_token_interactive.return_value = {"access_token": "token"} + msal_app_mock = mock.MagicMock() + msal_app_mock.CONSOLE_WINDOW_HANDLE = "console" + msal_app_mock.acquire_token_interactive.return_value = {"access_token": "token"} - result = identity.login_with_auth_code(["scope"]) + with mock.patch.object(Identity, "_msal_app", new_callable=mock.PropertyMock, return_value=msal_app_mock): + result = identity.login_with_auth_code(["scope"]) self.assertEqual(result, {"username": "user1"}) wsl_browser_open_mock.assert_called_once() - identity._msal_app_instance.acquire_token_interactive.assert_called_once() + msal_app_mock.acquire_token_interactive.assert_called_once() check_result_mock.assert_called_once_with({"access_token": "token"}) @mock.patch("azure.cli.core.auth.identity.ServicePrincipalStore.save_entry") From e7f52c89798745528dc9f279cc6d34126366778e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:15:20 +0000 Subject: [PATCH 06/11] Tighten WSL browser validation Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- src/azure-cli-core/azure/cli/core/tests/test_util.py | 3 ++- src/azure-cli-core/azure/cli/core/util.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_util.py b/src/azure-cli-core/azure/cli/core/tests/test_util.py index 999b255884e..47ffa20af44 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_util.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_util.py @@ -238,9 +238,10 @@ def test_is_wsl_interop_enabled_with_late_entry(self, listdir_mock, _): m.assert_called_once_with('/proc/sys/fs/binfmt_misc/WSLInterop-late', 'r') @mock.patch('azure.cli.core.util._open_url_in_wsl_browser', autospec=True, return_value=True) + @mock.patch('azure.cli.core.util._is_wsl_interop_enabled', autospec=True, return_value=True) @mock.patch('azure.cli.core.util._get_platform_info', autospec=True, return_value=('linux', '5.10.16.3-microsoft-standard-WSL2')) - def test_wsl_browser_open_uses_wsl_browser(self, _, open_url_mock): + def test_wsl_browser_open_uses_wsl_browser(self, _, __, open_url_mock): import webbrowser original_open = webbrowser.open diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index 469c1073acc..0ce47f1b765 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -866,7 +866,7 @@ def _is_safe_browser_url(url): except ValueError: return False - return parsed_url.scheme in ('http', 'https') and not any(c in url for c in '\n\r\t') + return parsed_url.scheme in ('http', 'https') and not any(ord(c) < 32 or ord(c) == 127 for c in url) def _open_url_in_wsl_browser(url): @@ -888,7 +888,7 @@ class _WslBrowserOpen: def __enter__(self): self._original_open = None self._patch_applied = False - if not is_wsl(): + if not is_wsl() or not _is_wsl_interop_enabled(): return self import webbrowser From 1c17482bf69fda066749c2bcf8090e41c876f1f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:16:20 +0000 Subject: [PATCH 07/11] Clean up WSL browser validation Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- src/azure-cli-core/azure/cli/core/tests/test_util.py | 6 ++++-- src/azure-cli-core/azure/cli/core/util.py | 7 +++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_util.py b/src/azure-cli-core/azure/cli/core/tests/test_util.py index 47ffa20af44..e28a798d680 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_util.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_util.py @@ -182,7 +182,7 @@ def test_open_page_in_browser(self, subprocess_open_mock, webbrowser_open_mock): @mock.patch('azure.cli.core.util._get_platform_info', autospec=True) @mock.patch('azure.cli.core.util._is_wsl_interop_enabled', autospec=True, return_value=True) @mock.patch('webbrowser.get', autospec=True) - def test_can_launch_browser(self, webbrowser_get_mock, _, get_platform_mock, which_mock): + def test_can_launch_browser(self, webbrowser_get_mock, wsl_interop_mock, get_platform_mock, which_mock): import webbrowser # Windows is always fine @@ -225,6 +225,7 @@ def test_can_launch_browser(self, webbrowser_get_mock, _, get_platform_mock, whi webbrowser_get_mock.side_effect = webbrowser.Error which_mock.return_value = False assert not can_launch_browser() + self.assertTrue(wsl_interop_mock.called) @mock.patch('azure.cli.core.util._get_platform_info', autospec=True, return_value=('linux', '5.10.16.3-microsoft-standard-WSL2')) @@ -241,7 +242,7 @@ def test_is_wsl_interop_enabled_with_late_entry(self, listdir_mock, _): @mock.patch('azure.cli.core.util._is_wsl_interop_enabled', autospec=True, return_value=True) @mock.patch('azure.cli.core.util._get_platform_info', autospec=True, return_value=('linux', '5.10.16.3-microsoft-standard-WSL2')) - def test_wsl_browser_open_uses_wsl_browser(self, _, __, open_url_mock): + def test_wsl_browser_open_uses_wsl_browser(self, _, wsl_interop_mock, open_url_mock): import webbrowser original_open = webbrowser.open @@ -249,6 +250,7 @@ def test_wsl_browser_open_uses_wsl_browser(self, _, __, open_url_mock): assert webbrowser.open('https://login.example.com') open_url_mock.assert_called_once_with('https://login.example.com') + wsl_interop_mock.assert_called_once() self.assertEqual(webbrowser.open, original_open) @mock.patch('subprocess.call', autospec=True) diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index 0ce47f1b765..f43c3b4dfde 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -861,11 +861,10 @@ def _get_wsl_browser_commands(url): def _is_safe_browser_url(url): from urllib.parse import urlparse - try: - parsed_url = urlparse(url) - except ValueError: + if not isinstance(url, str): return False + parsed_url = urlparse(url) return parsed_url.scheme in ('http', 'https') and not any(ord(c) < 32 or ord(c) == 127 for c in url) @@ -888,7 +887,7 @@ class _WslBrowserOpen: def __enter__(self): self._original_open = None self._patch_applied = False - if not is_wsl() or not _is_wsl_interop_enabled(): + if not _is_wsl_interop_enabled(): return self import webbrowser From a418f7c7fd9ba4383dbaac2e9211b5f8d6f11142 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:17:35 +0000 Subject: [PATCH 08/11] Harden WSL browser launch command Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- .../azure/cli/core/tests/test_util.py | 13 ++++++++++++- src/azure-cli-core/azure/cli/core/util.py | 13 +++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_util.py b/src/azure-cli-core/azure/cli/core/tests/test_util.py index e28a798d680..d125221f2dc 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_util.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_util.py @@ -250,7 +250,7 @@ def test_wsl_browser_open_uses_wsl_browser(self, _, wsl_interop_mock, open_url_m assert webbrowser.open('https://login.example.com') open_url_mock.assert_called_once_with('https://login.example.com') - wsl_interop_mock.assert_called_once() + wsl_interop_mock.assert_called() self.assertEqual(webbrowser.open, original_open) @mock.patch('subprocess.call', autospec=True) @@ -259,6 +259,17 @@ def test_open_url_in_wsl_browser_rejects_non_http_url(self, _, subprocess_call_m assert not _open_url_in_wsl_browser('/C:/Windows/System32/calc.exe') subprocess_call_mock.assert_not_called() + @mock.patch('subprocess.call', autospec=True, return_value=0) + @mock.patch('azure.cli.core.util._is_wsl_interop_enabled', autospec=True, return_value=True) + def test_open_url_in_wsl_browser_passes_url_by_env(self, _, subprocess_call_mock): + url = 'https://login.example.com/path?x=1&y=2' + + assert _open_url_in_wsl_browser(url) + + cmd = subprocess_call_mock.call_args.args[0] + self.assertNotIn(url, cmd) + self.assertEqual(subprocess_call_mock.call_args.kwargs['env']['AZURE_CLI_WSL_BROWSER_URL'], url) + def test_configured_default_setter(self): config = mock.MagicMock() config.use_local_config = None diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index f43c3b4dfde..9bdbb5cdd19 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -849,12 +849,11 @@ def _is_wsl_interop_enabled(): return bool(os.environ.get('WSL_INTEROP')) -def _get_wsl_browser_commands(url): - safe_url = url.replace("'", "''") +def _get_wsl_browser_commands(): return [ (['/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe', - '-NoProfile', '-Command', f"Start-Process '{safe_url}'"], (0,)), - (['powershell.exe', '-NoProfile', '-Command', f"Start-Process '{safe_url}'"], (0,)) + '-NoProfile', '-Command', 'Start-Process $env:AZURE_CLI_WSL_BROWSER_URL'], (0,)), + (['powershell.exe', '-NoProfile', '-Command', 'Start-Process $env:AZURE_CLI_WSL_BROWSER_URL'], (0,)) ] @@ -873,9 +872,11 @@ def _open_url_in_wsl_browser(url): return False import subprocess - for cmd, success_codes in _get_wsl_browser_commands(url): + env = os.environ.copy() + env['AZURE_CLI_WSL_BROWSER_URL'] = url + for cmd, success_codes in _get_wsl_browser_commands(): try: - exit_code = subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + exit_code = subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env) if exit_code in success_codes: return True except OSError: From 5df0de7c5a185a84ced6e721b4a34703a2d68970 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:18:38 +0000 Subject: [PATCH 09/11] Serialize WSL browser patching Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- src/azure-cli-core/azure/cli/core/tests/test_util.py | 1 + src/azure-cli-core/azure/cli/core/util.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_util.py b/src/azure-cli-core/azure/cli/core/tests/test_util.py index d125221f2dc..e52aed1f762 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_util.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_util.py @@ -257,6 +257,7 @@ def test_wsl_browser_open_uses_wsl_browser(self, _, wsl_interop_mock, open_url_m @mock.patch('azure.cli.core.util._is_wsl_interop_enabled', autospec=True, return_value=True) def test_open_url_in_wsl_browser_rejects_non_http_url(self, _, subprocess_call_mock): assert not _open_url_in_wsl_browser('/C:/Windows/System32/calc.exe') + assert not _open_url_in_wsl_browser('https:') subprocess_call_mock.assert_not_called() @mock.patch('subprocess.call', autospec=True, return_value=0) diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index 9bdbb5cdd19..1fec0ef5ea4 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -10,6 +10,7 @@ import platform import re import sys +import threading from knack.log import get_logger from knack.util import CLIError, to_snake_case, to_camel_case @@ -18,6 +19,7 @@ CLI_PACKAGE_NAME = 'azure-cli' COMPONENT_PREFIX = 'azure-cli-' +_WSL_BROWSER_OPEN_LOCK = threading.Lock() SSLERROR_TEMPLATE = ('Certificate verification failed. This typically happens when using Azure CLI behind a proxy ' 'that intercepts traffic with a self-signed certificate. ' @@ -864,7 +866,8 @@ def _is_safe_browser_url(url): return False parsed_url = urlparse(url) - return parsed_url.scheme in ('http', 'https') and not any(ord(c) < 32 or ord(c) == 127 for c in url) + return (parsed_url.scheme in ('http', 'https') and bool(parsed_url.netloc) and + not any(ord(c) < 32 or ord(c) == 127 for c in url)) def _open_url_in_wsl_browser(url): @@ -888,9 +891,13 @@ class _WslBrowserOpen: def __enter__(self): self._original_open = None self._patch_applied = False + self._lock_acquired = False if not _is_wsl_interop_enabled(): return self + _WSL_BROWSER_OPEN_LOCK.acquire() + self._lock_acquired = True + import webbrowser self._original_open = webbrowser.open @@ -905,6 +912,8 @@ def __exit__(self, *args): if self._patch_applied: import webbrowser webbrowser.open = self._original_open + if self._lock_acquired: + _WSL_BROWSER_OPEN_LOCK.release() def wsl_browser_open(): From 1ae3afcfe2184800c007a6cf130f754836ef0abd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:19:35 +0000 Subject: [PATCH 10/11] Guard WSL browser patch calls Co-authored-by: a0x1ab <59631311+a0x1ab@users.noreply.github.com> --- src/azure-cli-core/azure/cli/core/util.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index 1fec0ef5ea4..ef14c0cd3b1 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -19,7 +19,7 @@ CLI_PACKAGE_NAME = 'azure-cli' COMPONENT_PREFIX = 'azure-cli-' -_WSL_BROWSER_OPEN_LOCK = threading.Lock() +_WSL_BROWSER_OPEN_LOCK = threading.RLock() SSLERROR_TEMPLATE = ('Certificate verification failed. This typically happens when using Azure CLI behind a proxy ' 'that intercepts traffic with a self-signed certificate. ' @@ -881,6 +881,7 @@ def _open_url_in_wsl_browser(url): try: exit_code = subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env) if exit_code in success_codes: + # Start-Process returns after handing off the URL to Windows. return True except OSError: continue @@ -902,7 +903,8 @@ def __enter__(self): self._original_open = webbrowser.open def _open(url, *args, **kwargs): - return _open_url_in_wsl_browser(url) or self._original_open(url, *args, **kwargs) + with _WSL_BROWSER_OPEN_LOCK: + return _open_url_in_wsl_browser(url) or self._original_open(url, *args, **kwargs) webbrowser.open = _open self._patch_applied = True From 27887737be02886da4ef41abe0bbb2b034449a6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 06:23:17 +0000 Subject: [PATCH 11/11] Fix WSL browser lint issues Co-authored-by: coopercox-ms <264909354+coopercox-ms@users.noreply.github.com> --- src/azure-cli-core/azure/cli/core/auth/identity.py | 3 +-- src/azure-cli-core/azure/cli/core/util.py | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/auth/identity.py b/src/azure-cli-core/azure/cli/core/auth/identity.py index 432e38d142a..fcce8808b58 100644 --- a/src/azure-cli-core/azure/cli/core/auth/identity.py +++ b/src/azure-cli-core/azure/cli/core/auth/identity.py @@ -9,12 +9,11 @@ import sys from azure.cli.core._environment import get_config_dir +from azure.cli.core.util import wsl_browser_open from knack.log import get_logger from knack.util import CLIError from msal import PublicClientApplication, ConfidentialClientApplication -from azure.cli.core.util import wsl_browser_open - from .constants import AZURE_CLI_CLIENT_ID from .msal_credentials import UserCredential, ServicePrincipalCredential from .persistence import load_persisted_token_cache, file_extensions, load_secret_store diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index ef14c0cd3b1..a820206f05c 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -889,6 +889,11 @@ def _open_url_in_wsl_browser(url): class _WslBrowserOpen: + def __init__(self): + self._original_open = None + self._patch_applied = False + self._lock_acquired = False + def __enter__(self): self._original_open = None self._patch_applied = False