Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/curl_install_pypi/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from urllib.request import urlopen

AZ_DISPATCH_TEMPLATE = """#!/usr/bin/env bash
{install_dir}/bin/python -m azure.cli "$@"
{install_dir}/bin/python -c 'import os, runpy, sys; cwd = os.path.normcase(os.path.realpath(os.getcwd())); sys.path[:] = [path for path in sys.path if os.path.normcase(os.path.realpath(path or os.curdir)) != cwd]; runpy.run_module("azure.cli.__main__", run_name="__main__", alter_sys=True)' "$@"
"""

VIRTUALENV_VERSION = '16.7.11'
Expand Down
2 changes: 1 addition & 1 deletion scripts/release/macos/templates/az_launcher.sh.in
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,4 @@ fi

export PYTHONPATH="$AZURE_CLI_SITE_PACKAGES"
export AZ_INSTALLER="$INSTALLER"
exec "$PYTHON" -sm azure.cli "$@"
exec "$PYTHON" -s "$AZURE_CLI_SITE_PACKAGES/azure/cli/__main__.py" "$@"
3 changes: 2 additions & 1 deletion scripts/release/rpm/azure-cli.spec
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ printf "#!/usr/bin/env bash
bin_dir=\`cd \"\$(dirname \"\$BASH_SOURCE[0]\")\"; pwd\`
python_cmd=%{python_cmd}
if command -v ${python_version} &>/dev/null; then python_cmd=${python_version}; fi
AZ_INSTALLER=RPM PYTHONPATH=\"\$bin_dir/../lib64/az/lib/${python_version}/site-packages\" \$python_cmd -sm azure.cli \"\$@\"
site_packages=\"\$bin_dir/../lib64/az/lib/${python_version}/site-packages\"
AZ_INSTALLER=RPM PYTHONPATH=\"\$site_packages\" \$python_cmd -s \"\$site_packages/azure/cli/__main__.py\" \"\$@\"
" > %{buildroot}%{_bindir}/az
rm %{buildroot}%{cli_lib_dir}/bin/python* %{buildroot}%{cli_lib_dir}/bin/pip*

Expand Down
131 changes: 131 additions & 0 deletions scripts/release/tests/test_launcher_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import os
from pathlib import Path
import runpy
import shutil
import subprocess
import sys
import tempfile
import unittest


REPO_ROOT = Path(__file__).resolve().parents[3]
MACOS_LAUNCHER = REPO_ROOT / 'scripts/release/macos/templates/az_launcher.sh.in'
CURL_INSTALLER = REPO_ROOT / 'scripts/curl_install_pypi/install.py'


class LauncherSecurityTest(unittest.TestCase):

def _write_cli_package(self, site_packages, exit_code):
cli_dir = site_packages / 'azure' / 'cli'
cli_dir.mkdir(parents=True)
(cli_dir.parent / '__init__.py').write_text('', encoding='utf-8')
(cli_dir / '__init__.py').write_text('', encoding='utf-8')
(cli_dir / '__main__.py').write_text(
'import json, sys\n'
'print(json.dumps(sys.argv[1:]))\n'
f'raise SystemExit({exit_code})\n',
encoding='utf-8',
)

def _write_attacker_module(self, attacker_dir):
attacker_dir.mkdir()
(attacker_dir / 'azure.py').write_text(
'from pathlib import Path\n'
'Path("PWNED").write_text("yes", encoding="utf-8")\n',
encoding='utf-8',
)

def test_macos_launcher_ignores_cwd_module(self):
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
install_dir = root / 'install'
launcher_dir = install_dir / 'libexec' / 'bin'
site_packages = install_dir / 'libexec' / 'lib' / \
f'python{sys.version_info.major}.{sys.version_info.minor}' / 'site-packages'
attacker_dir = root / 'attacker'
launcher_dir.mkdir(parents=True)
self._write_cli_package(site_packages, exit_code=23)
self._write_attacker_module(attacker_dir)

launcher = launcher_dir / 'az'
launcher.write_text(
MACOS_LAUNCHER.read_text(encoding='utf-8')
.replace('{PYTHON_MAJOR_MINOR}', f'{sys.version_info.major}.{sys.version_info.minor}')
.replace('{PYTHON_BIN}', f'python{sys.version_info.major}'),
encoding='utf-8',
)
launcher.chmod(0o755)

result = subprocess.run(
[str(launcher), 'alpha', 'two words'],
cwd=attacker_dir,
env={**os.environ, 'AZ_PYTHON': sys.executable},
check=False,
capture_output=True,
text=True,
)

self.assertEqual(23, result.returncode, result.stderr)
self.assertEqual('["alpha", "two words"]', result.stdout.strip())
self.assertFalse((attacker_dir / 'PWNED').exists())

def test_curl_dispatch_ignores_cwd_module(self):
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
install_dir = root / 'install'
bin_dir = install_dir / 'bin'
site_packages = install_dir / 'site-packages'
attacker_dir = root / 'attacker'
bin_dir.mkdir(parents=True)
self._write_cli_package(site_packages, exit_code=31)
self._write_attacker_module(attacker_dir)

python_wrapper = bin_dir / 'python'
python_wrapper.write_text(
'#!/usr/bin/env bash\n'
f'PYTHONPATH="{site_packages}" exec "{sys.executable}" "$@"\n',
encoding='utf-8',
)
python_wrapper.chmod(0o755)

dispatch_template = runpy.run_path(str(CURL_INSTALLER))['AZ_DISPATCH_TEMPLATE']
launcher = root / 'az'
launcher.write_text(dispatch_template.format(install_dir=install_dir), encoding='utf-8')
launcher.chmod(0o755)

result = subprocess.run(
[str(launcher), 'alpha', 'two words'],
cwd=attacker_dir,
check=False,
capture_output=True,
text=True,
)

self.assertEqual(31, result.returncode, result.stderr)
self.assertEqual('["alpha", "two words"]', result.stdout.strip())
self.assertFalse((attacker_dir / 'PWNED').exists())

def test_production_launchers_do_not_use_unsafe_module_entry(self):
launcher_paths = [
REPO_ROOT / 'src/azure-cli/az.bat',
REPO_ROOT / 'src/azure-cli/azps.ps1',
REPO_ROOT / 'scripts/curl_install_pypi/install.py',
REPO_ROOT / 'scripts/release/macos/templates/az_launcher.sh.in',
REPO_ROOT / 'scripts/release/rpm/azure-cli.spec',
REPO_ROOT / 'src/azure-cli/azure/cli/command_modules/cognitiveservices/custom.py',
]

for launcher_path in launcher_paths:
with self.subTest(launcher_path=launcher_path):
contents = launcher_path.read_text(encoding='utf-8')
self.assertNotIn('-m azure.cli', contents)
self.assertNotIn("'-m', 'azure.cli'", contents)
Comment on lines +125 to +127


if __name__ == '__main__':
unittest.main()
12 changes: 8 additions & 4 deletions src/azure-cli/az.bat
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
@echo off
setlocal

SET PYTHONPATH=%~dp0\src;%PYTHONPATH%
SET AZ_INSTALLER=PIP
IF EXIST "%~dp0src\azure\cli\__main__.py" (
SET "PYTHONPATH=%~dp0src;%PYTHONPATH%"
) ELSE (
SET "PYTHONPATH=%~dp0;%PYTHONPATH%"
)
SET "AZ_INSTALLER=PIP"

IF EXIST "%~dp0\python.exe" (
"%~dp0\python.exe" -m azure.cli %*
"%~dp0\python.exe" -c "import os,runpy,sys; cwd=os.path.normcase(os.path.realpath(os.getcwd())); sys.path[:]=[path for path in sys.path if os.path.normcase(os.path.realpath(path or os.curdir)) != cwd]; runpy.run_module('azure.cli.__main__', run_name='__main__', alter_sys=True)" %*
) ELSE (
python -m azure.cli %*
python -c "import os,runpy,sys; cwd=os.path.normcase(os.path.realpath(os.getcwd())); sys.path[:]=[path for path in sys.path if os.path.normcase(os.path.realpath(path or os.curdir)) != cwd]; runpy.run_module('azure.cli.__main__', run_name='__main__', alter_sys=True)" %*
)
15 changes: 12 additions & 3 deletions src/azure-cli/azps.ps1
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
$env:AZ_INSTALLER="PIP"
$moduleSearchPath = if (Test-Path "$PSScriptRoot\src\azure\cli\__main__.py") {
"$PSScriptRoot\src"
}
else {
$PSScriptRoot
}
$env:PYTHONPATH = "$moduleSearchPath$([IO.Path]::PathSeparator)$env:PYTHONPATH"
$launcherCode = 'import os,runpy,sys; cwd=os.path.normcase(os.path.realpath(os.getcwd())); sys.path[:]=[path for path in sys.path if os.path.normcase(os.path.realpath(path or os.curdir)) != cwd]; runpy.run_module("azure.cli.__main__", run_name="__main__", alter_sys=True)'

if (Test-Path "$PSScriptRoot\python.exe") {
# Perfer python.exe in venv
& "$PSScriptRoot\python.exe" -m azure.cli $args
# Prefer python.exe in venv
& "$PSScriptRoot\python.exe" -c $launcherCode $args
}
else {
# Run system python.exe
python.exe -m azure.cli $args
python.exe -c $launcherCode $args
}
exit $LASTEXITCODE

# SIG # Begin signature block
# MIInqgYJKoZIhvcNAQcCoIInmzCCJ5cCAQExDzANBglghkgBZQMEAgEFADB5Bgor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -765,11 +765,8 @@ def _push_image_to_registry(_cmd, image_name, registry_name): # pylint: disable

try:
# Login to ACR using Azure CLI credentials
# We use sys.executable + '-m azure.cli' instead of 'az' command to ensure
# compatibility when developing/testing the CLI from source. In development
# environments, 'az' may not be in PATH or may point to a different installation.
# In production, sys.executable will still correctly invoke the installed CLI.
az_cmd = [sys.executable, '-m', 'azure.cli', 'acr', 'login', '--name', registry_name]
azure_cli_main = Path(__file__).resolve().parents[2] / '__main__.py'
az_cmd = [sys.executable, str(azure_cli_main), 'acr', 'login', '--name', registry_name]
logger.info("Logging into ACR: %s", registry_name)

result = subprocess.run(
Expand Down
Loading