Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package salt for openSUSE:Factory checked in 
at 2022-03-01 17:03:12
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/salt (Old)
 and      /work/SRC/openSUSE:Factory/.salt.new.1958 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "salt"

Tue Mar  1 17:03:12 2022 rev:126 rq:958078 version:3004

Changes:
--------
--- /work/SRC/openSUSE:Factory/salt/salt.changes        2022-02-11 
23:07:25.930609617 +0100
+++ /work/SRC/openSUSE:Factory/.salt.new.1958/salt.changes      2022-03-01 
17:03:28.612331991 +0100
@@ -1,0 +2,25 @@
+Mon Feb 28 15:05:32 UTC 2022 - Pablo Su??rez Hern??ndez 
<pablo.suarezhernan...@suse.com>
+
+- Fix issues found around pre_flight_script_args
+
+- Added:
+  * prevent-shell-injection-via-pre_flight_script_args-4.patch
+
+-------------------------------------------------------------------
+Thu Feb 24 14:06:44 UTC 2022 - Victor Zhestkov <victor.zhest...@suse.com>
+
+- Add salt-ssh with Salt Bundle support (venv-salt-minion)
+  (bsc#1182851, bsc#1196432)
+
+- Added:
+  * add-salt-ssh-support-with-venv-salt-minion-3004-493.patch
+
+-------------------------------------------------------------------
+Thu Feb 17 15:27:00 UTC 2022 - Pablo Su??rez Hern??ndez 
<pablo.suarezhernan...@suse.com>
+
+- Restrict "state.orchestrate_single" to pass a pillar value if it exists 
(bsc#1194632)
+
+- Added:
+  * state.orchestrate_single-does-not-pass-pillar-none-4.patch
+
+-------------------------------------------------------------------

New:
----
  add-salt-ssh-support-with-venv-salt-minion-3004-493.patch
  prevent-shell-injection-via-pre_flight_script_args-4.patch
  state.orchestrate_single-does-not-pass-pillar-none-4.patch

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ salt.spec ++++++
--- /var/tmp/diff_new_pack.yPgP67/_old  2022-03-01 17:03:30.024332366 +0100
+++ /var/tmp/diff_new_pack.yPgP67/_new  2022-03-01 17:03:30.028332367 +0100
@@ -279,6 +279,16 @@
 Patch70:        add-missing-ansible-module-functions-to-whitelist-in.patch
 # PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/61256
 Patch71:        fix-salt-call-event.send-call-with-grains-and-pillar.patch
+# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/61093
+Patch72:        state.orchestrate_single-does-not-pass-pillar-none-4.patch
+
+### SALT-SSH WITH SALT BUNDLE ###
+# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/61715 
(ssh_pre_flight_args)
+# PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/493
+# PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/497
+Patch73:        add-salt-ssh-support-with-venv-salt-minion-3004-493.patch
+Patch74:        prevent-shell-injection-via-pre_flight_script_args-4.patch
+###############
 
 
 BuildRoot:      %{_tmppath}/%{name}-%{version}-build

++++++ _lastrevision ++++++
--- /var/tmp/diff_new_pack.yPgP67/_old  2022-03-01 17:03:30.092332384 +0100
+++ /var/tmp/diff_new_pack.yPgP67/_new  2022-03-01 17:03:30.096332385 +0100
@@ -1,3 +1,3 @@
-e2c4840e38a6c4ab52bdba40139d9fb461d9b754
+8fe3232b41facbf938d591053c0f457ba6b5e3dc
 (No newline at EOF)
 

++++++ add-salt-ssh-support-with-venv-salt-minion-3004-493.patch ++++++
++++ 841 lines (skipped)

++++++ prevent-shell-injection-via-pre_flight_script_args-4.patch ++++++
>From 24093156ace91a8766eb1f5acbc47eee8e634d8e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?=
 <psuarezhernan...@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



++++++ state.orchestrate_single-does-not-pass-pillar-none-4.patch ++++++
>From d44207dc209894b36f2a2c8af4c81afcd86b4625 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?=
 <psuarezhernan...@suse.com>
Date: Wed, 9 Feb 2022 09:01:08 +0000
Subject: [PATCH] state.orchestrate_single does not pass pillar=None
 (#488)

Passing a pillar to state.single can results in state functions not
working when they don't accept a pillar keyword argument. One example
where this is the case is salt.wait_for_event. When no pillar is
provided, it does not need to be passed.

Co-authored-by: Alexander Graul <agr...@suse.com>
---
 changelog/61092.fixed                    |  3 ++
 salt/runners/state.py                    | 10 ++++--
 tests/pytests/unit/runners/test_state.py | 41 ++++++++++++++++++++++++
 3 files changed, 51 insertions(+), 3 deletions(-)
 create mode 100644 changelog/61092.fixed
 create mode 100644 tests/pytests/unit/runners/test_state.py

diff --git a/changelog/61092.fixed b/changelog/61092.fixed
new file mode 100644
index 0000000000..6ca66839c9
--- /dev/null
+++ b/changelog/61092.fixed
@@ -0,0 +1,3 @@
+state.orchestrate_single only passes a pillar if it is set to the state
+function. This allows it to be used with state functions that don't accept a
+pillar keyword argument.
diff --git a/salt/runners/state.py b/salt/runners/state.py
index f8fc1b0944..5642204ce9 100644
--- a/salt/runners/state.py
+++ b/salt/runners/state.py
@@ -150,12 +150,16 @@ def orchestrate_single(fun, name, test=None, queue=False, 
pillar=None, **kwargs)
 
         salt-run state.orchestrate_single fun=salt.wheel name=key.list_all
     """
-    if pillar is not None and not isinstance(pillar, dict):
-        raise SaltInvocationError("Pillar data must be formatted as a 
dictionary")
+    if pillar is not None:
+        if isinstance(pillar, dict):
+            kwargs["pillar"] = pillar
+        else:
+            raise SaltInvocationError("Pillar data must be formatted as a 
dictionary")
+
     __opts__["file_client"] = "local"
     minion = salt.minion.MasterMinion(__opts__)
     running = minion.functions["state.single"](
-        fun, name, test=None, queue=False, pillar=pillar, **kwargs
+        fun, name, test=None, queue=False, **kwargs
     )
     ret = {minion.opts["id"]: running}
     __jid_event__.fire_event({"data": ret, "outputter": "highstate"}, 
"progress")
diff --git a/tests/pytests/unit/runners/test_state.py 
b/tests/pytests/unit/runners/test_state.py
new file mode 100644
index 0000000000..df0a718a41
--- /dev/null
+++ b/tests/pytests/unit/runners/test_state.py
@@ -0,0 +1,41 @@
+#!/usr/bin/python3
+
+import pytest
+from salt.runners import state as state_runner
+from tests.support.mock import Mock, patch
+
+
+@pytest.fixture
+def configure_loader_modules():
+    return {state_runner: {"__opts__": {}, "__jid_event__": Mock()}}
+
+
+def test_orchestrate_single_passes_pillar():
+    """
+    test state.orchestrate_single passes given pillar to state.single
+    """
+    mock_master_minion = Mock()
+    mock_state_single = Mock()
+    mock_master_minion.functions = {"state.single": mock_state_single}
+    mock_master_minion.opts = {"id": "dummy"}
+    test_pillar = {"test_entry": "exists"}
+    with patch("salt.minion.MasterMinion", 
Mock(return_value=mock_master_minion)):
+        state_runner.orchestrate_single(
+            fun="pillar.get", name="test_entry", pillar=test_pillar
+        )
+        assert mock_state_single.call_args.kwargs["pillar"] == test_pillar
+
+
+def test_orchestrate_single_does_not_pass_none_pillar():
+    """
+    test state.orchestrate_single does not pass pillar=None to state.single
+    """
+    mock_master_minion = Mock()
+    mock_state_single = Mock()
+    mock_master_minion.functions = {"state.single": mock_state_single}
+    mock_master_minion.opts = {"id": "dummy"}
+    with patch("salt.minion.MasterMinion", 
Mock(return_value=mock_master_minion)):
+        state_runner.orchestrate_single(
+            fun="pillar.get", name="test_entry", pillar=None
+        )
+        assert "pillar" not in mock_state_single.call_args.kwargs
-- 
2.35.1

Reply via email to