4f7bbe4131
OBS-URL: https://build.opensuse.org/package/show/systemsmanagement:saltstack/salt?expand=0&rev=197
192 lines
7.0 KiB
Diff
192 lines
7.0 KiB
Diff
From 24093156ace91a8766eb1f5acbc47eee8e634d8e Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?=
|
|
<psuarezhernandez@suse.com>
|
|
Date: Mon, 28 Feb 2022 14:25:43 +0000
|
|
Subject: [PATCH] Prevent shell injection via pre_flight_script_args
|
|
(#497)
|
|
|
|
Add tests around preflight script args
|
|
|
|
Readjust logic to validate script args
|
|
|
|
Use RLock to prevent issues in single threads
|
|
---
|
|
salt/_logging/impl.py | 2 +-
|
|
salt/client/ssh/__init__.py | 9 ++--
|
|
tests/integration/ssh/test_pre_flight.py | 56 ++++++++++++++++++++++--
|
|
tests/unit/client/test_ssh.py | 35 +++++++++++++++
|
|
4 files changed, 92 insertions(+), 10 deletions(-)
|
|
|
|
diff --git a/salt/_logging/impl.py b/salt/_logging/impl.py
|
|
index 953490b284..4f48672032 100644
|
|
--- a/salt/_logging/impl.py
|
|
+++ b/salt/_logging/impl.py
|
|
@@ -92,7 +92,7 @@ MODNAME_PATTERN = re.compile(r"(?P<name>%%\(name\)(?:\-(?P<digits>[\d]+))?s)")
|
|
|
|
# LOG_LOCK is used to prevent deadlocks on using logging
|
|
# in combination with multiprocessing with salt-api
|
|
-LOG_LOCK = threading.Lock()
|
|
+LOG_LOCK = threading.RLock()
|
|
|
|
|
|
# ----- REMOVE ME ON REFACTOR COMPLETE ------------------------------------------------------------------------------>
|
|
diff --git a/salt/client/ssh/__init__.py b/salt/client/ssh/__init__.py
|
|
index 0066f4597b..3e032c7197 100644
|
|
--- a/salt/client/ssh/__init__.py
|
|
+++ b/salt/client/ssh/__init__.py
|
|
@@ -15,6 +15,7 @@ import os
|
|
import psutil
|
|
import queue
|
|
import re
|
|
+import shlex
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
@@ -1458,11 +1459,9 @@ ARGS = {arguments}\n'''.format(
|
|
"""
|
|
args = ""
|
|
if script_args:
|
|
- args = " {}".format(
|
|
- " ".join([str(el) for el in script_args])
|
|
- if isinstance(script_args, (list, tuple))
|
|
- else script_args
|
|
- )
|
|
+ if not isinstance(script_args, (list, tuple)):
|
|
+ script_args = shlex.split(str(script_args))
|
|
+ args = " {}".format(" ".join([shlex.quote(str(el)) for el in script_args]))
|
|
if extension == "ps1":
|
|
ret = self.shell.exec_cmd('"powershell {}"'.format(script))
|
|
else:
|
|
diff --git a/tests/integration/ssh/test_pre_flight.py b/tests/integration/ssh/test_pre_flight.py
|
|
index 6233ec0fe7..9c39219e9d 100644
|
|
--- a/tests/integration/ssh/test_pre_flight.py
|
|
+++ b/tests/integration/ssh/test_pre_flight.py
|
|
@@ -25,10 +25,14 @@ class SSHPreFlightTest(SSHCase):
|
|
RUNTIME_VARS.TMP, "test-pre-flight-script-worked.txt"
|
|
)
|
|
|
|
- def _create_roster(self):
|
|
- self.custom_roster(self.roster, self.data)
|
|
+ def _create_roster(self, pre_flight_script_args=None):
|
|
+ data = dict(self.data)
|
|
+ if pre_flight_script_args:
|
|
+ data["ssh_pre_flight_args"] = pre_flight_script_args
|
|
|
|
- with salt.utils.files.fopen(self.data["ssh_pre_flight"], "w") as fp_:
|
|
+ self.custom_roster(self.roster, data)
|
|
+
|
|
+ with salt.utils.files.fopen(data["ssh_pre_flight"], "w") as fp_:
|
|
fp_.write("touch {}".format(self.test_script))
|
|
|
|
@pytest.mark.slow_test
|
|
@@ -58,6 +62,45 @@ class SSHPreFlightTest(SSHCase):
|
|
)
|
|
assert os.path.exists(self.test_script)
|
|
|
|
+ @pytest.mark.slow_test
|
|
+ def test_ssh_run_pre_flight_args(self):
|
|
+ """
|
|
+ test ssh when --pre-flight is passed to salt-ssh
|
|
+ to ensure the script runs successfully passing some args
|
|
+ """
|
|
+ self._create_roster(pre_flight_script_args="foobar test")
|
|
+ # make sure we previously ran a command so the thin dir exists
|
|
+ self.run_function("test.ping", wipe=False)
|
|
+ assert not os.path.exists(self.test_script)
|
|
+
|
|
+ assert self.run_function(
|
|
+ "test.ping", ssh_opts="--pre-flight", roster_file=self.roster, wipe=False
|
|
+ )
|
|
+ assert os.path.exists(self.test_script)
|
|
+
|
|
+ @pytest.mark.slow_test
|
|
+ def test_ssh_run_pre_flight_args_prevent_injection(self):
|
|
+ """
|
|
+ test ssh when --pre-flight is passed to salt-ssh
|
|
+ and evil arguments are used in order to produce shell injection
|
|
+ """
|
|
+ injected_file = os.path.join(RUNTIME_VARS.TMP, "injection")
|
|
+ self._create_roster(
|
|
+ pre_flight_script_args="foobar; echo injected > {}".format(injected_file)
|
|
+ )
|
|
+ # make sure we previously ran a command so the thin dir exists
|
|
+ self.run_function("test.ping", wipe=False)
|
|
+ assert not os.path.exists(self.test_script)
|
|
+ assert not os.path.isfile(injected_file)
|
|
+
|
|
+ assert self.run_function(
|
|
+ "test.ping", ssh_opts="--pre-flight", roster_file=self.roster, wipe=False
|
|
+ )
|
|
+
|
|
+ assert not os.path.isfile(
|
|
+ injected_file
|
|
+ ), "File injection suceeded. This shouldn't happend"
|
|
+
|
|
@pytest.mark.slow_test
|
|
def test_ssh_run_pre_flight_failure(self):
|
|
"""
|
|
@@ -77,7 +120,12 @@ class SSHPreFlightTest(SSHCase):
|
|
"""
|
|
make sure to clean up any old ssh directories
|
|
"""
|
|
- files = [self.roster, self.data["ssh_pre_flight"], self.test_script]
|
|
+ files = [
|
|
+ self.roster,
|
|
+ self.data["ssh_pre_flight"],
|
|
+ self.test_script,
|
|
+ os.path.join(RUNTIME_VARS.TMP, "injection"),
|
|
+ ]
|
|
for fp_ in files:
|
|
if os.path.exists(fp_):
|
|
os.remove(fp_)
|
|
diff --git a/tests/unit/client/test_ssh.py b/tests/unit/client/test_ssh.py
|
|
index 6f3d87d493..23cb3d0700 100644
|
|
--- a/tests/unit/client/test_ssh.py
|
|
+++ b/tests/unit/client/test_ssh.py
|
|
@@ -234,6 +234,41 @@ class SSHSingleTests(TestCase):
|
|
mock_flight.assert_called()
|
|
assert ret == cmd_ret
|
|
|
|
+ def test_run_with_pre_flight_with_args(self):
|
|
+ """
|
|
+ test Single.run() when ssh_pre_flight is set
|
|
+ and script successfully runs
|
|
+ """
|
|
+ target = self.target.copy()
|
|
+ target["ssh_pre_flight"] = os.path.join(RUNTIME_VARS.TMP, "script.sh")
|
|
+ target["ssh_pre_flight_args"] = "foobar"
|
|
+ single = ssh.Single(
|
|
+ self.opts,
|
|
+ self.opts["argv"],
|
|
+ "localhost",
|
|
+ mods={},
|
|
+ fsclient=None,
|
|
+ thin=salt.utils.thin.thin_path(self.opts["cachedir"]),
|
|
+ mine=False,
|
|
+ **target
|
|
+ )
|
|
+
|
|
+ cmd_ret = ("Success", "foobar", 0)
|
|
+ mock_flight = MagicMock(return_value=cmd_ret)
|
|
+ mock_cmd = MagicMock(return_value=cmd_ret)
|
|
+ patch_flight = patch("salt.client.ssh.Single.run_ssh_pre_flight", mock_flight)
|
|
+ patch_cmd = patch("salt.client.ssh.Single.cmd_block", mock_cmd)
|
|
+ patch_exec_cmd = patch(
|
|
+ "salt.client.ssh.shell.Shell.exec_cmd", return_value=("", "", 1)
|
|
+ )
|
|
+ patch_os = patch("os.path.exists", side_effect=[True])
|
|
+
|
|
+ with patch_os, patch_flight, patch_cmd, patch_exec_cmd:
|
|
+ ret = single.run()
|
|
+ mock_cmd.assert_called()
|
|
+ mock_flight.assert_called()
|
|
+ assert ret == cmd_ret
|
|
+
|
|
def test_run_with_pre_flight_stderr(self):
|
|
"""
|
|
test Single.run() when ssh_pre_flight is set
|
|
--
|
|
2.35.1
|
|
|
|
|