Skip to content

Commit 97ee957

Browse files
authored
{Core} Fix injection risk during extension dynamic installation (#33990)
1 parent 3e0fedf commit 97ee957

2 files changed

Lines changed: 38 additions & 6 deletions

File tree

‎src/azure-cli-core/azure/cli/core/extension/dynamic_install.py‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -241,13 +241,17 @@ def _check_value_in_extensions(cli_ctx, parser, args, no_prompt): # pylint: dis
241241
from azure.cli.core.extension.operations import add_extension
242242
add_extension(cli_ctx=cli_ctx, extension_name=ext_name, upgrade=True, allow_preview=extension_allow_preview)
243243
if run_after_extension_installed:
244-
import subprocess
245-
import platform
246-
exit_code = subprocess.call(args, shell=platform.system() == 'Windows')
244+
# Rerun the command with the CLI's own entry point instead of handing the raw argv back to a
245+
# shell. The previous implementation used `subprocess.call(args, shell=True)` on Windows, which
246+
# let cmd.exe reinterpret user-controlled arguments (e.g. `&`, `|`, `^`) and allowed command
247+
# injection. `run_az_cmd` executes the command in-process with the args kept as a list, so no
248+
# shell is involved.
249+
from azure.cli.core.util import run_az_cmd
250+
result = run_az_cmd(args[1:])
251+
exit_code = getattr(result, 'exit_code', 0)
247252
# In this case, error msg is for telemetry recording purpose only.
248-
# From UX perspective, the command will rerun in subprocess. Whether it succeeds or fails,
249-
# mesages will be shown from the subprocess and this process should not print more message to
250-
# interrupt that.
253+
# From UX perspective, the command is rerun and its messages are already shown,
254+
# so this process should not print more message to interrupt that.
251255
print_error = False
252256
error_msg = ("Extension {} dynamically installed{} and commands will be "
253257
"rerun automatically.").format(ext_name, prompt_info)

‎src/azure-cli-core/azure/cli/core/tests/test_parser.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,34 @@ def mock_add_extension(*args, **kwargs):
274274
parser.parse_args('test new-ext reset pos1 pos2'.split()) # test positional args
275275
self.assertIn("Extension another-ext-name installed. Please rerun your command.", logger_msgs[6])
276276

277+
# test dynamic extension install with the command rerun after installation.
278+
# The command must be rerun via run_az_cmd (args as a list, no shell) to avoid command injection.
279+
from knack.util import CommandResultItem
280+
run_az_cmd_args = []
281+
282+
def mock_run_az_cmd(args, out_file=None): # pylint: disable=unused-argument
283+
run_az_cmd_args.append(args)
284+
return CommandResultItem(None, exit_code=0)
285+
286+
with mock.patch.object(logging.Logger, 'error', mock_log_error), \
287+
mock.patch.object(azure.cli.core.extension.operations, 'add_extension', mock_add_extension), \
288+
mock.patch.object(azure.cli.core.extension.dynamic_install, '_get_extension_command_tree',
289+
mock_ext_cmd_tree_load), \
290+
mock.patch.object(azure.cli.core.extension.dynamic_install, '_get_extension_use_dynamic_install_config',
291+
return_value='yes_without_prompt'), \
292+
mock.patch.object(azure.cli.core.extension.dynamic_install,
293+
'_get_extension_run_after_dynamic_install_config', return_value=True), \
294+
mock.patch.object(azure.cli.core.util, 'run_az_cmd', mock_run_az_cmd):
295+
with self.assertRaises(SystemExit) as cm:
296+
parser.parse_args(['test', 'new-ext', 'create', '--opt', 'enum_2 & echo injected'])
297+
# exit code comes from the rerun result
298+
self.assertEqual(cm.exception.code, 0)
299+
# args are passed as a list without the 'az' entry point, so no shell can reinterpret them
300+
self.assertEqual(run_az_cmd_args,
301+
[['test', 'new-ext', 'create', '--opt', 'enum_2 & echo injected']])
302+
# error message is only for telemetry, it should not be printed again
303+
self.assertEqual(len(logger_msgs), 7)
304+
277305

278306
class VerifyError: # pylint: disable=too-few-public-methods
279307

0 commit comments

Comments
 (0)