From c1fe687c3744fec10650262733a785b73d1310e3ca527fbe9263f465b9759ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= Date: Wed, 4 Nov 2020 10:58:48 +0000 Subject: [PATCH 1/2] osc copypac from project:systemsmanagement:saltstack:testing package:salt revision:364 OBS-URL: https://build.opensuse.org/package/show/systemsmanagement:saltstack/salt?expand=0&rev=176 --- _lastrevision | 2 +- ...2020-25592-and-add-tests-bsc-1178319.patch | 581 ++++++++++++++++++ fix-novendorchange-option-284.patch | 240 ++++++++ ...ctools.wraps-with-six.wraps-bsc-1177.patch | 54 ++ salt.changes | 26 + salt.spec | 9 + 6 files changed, 911 insertions(+), 1 deletion(-) create mode 100644 fix-cve-2020-25592-and-add-tests-bsc-1178319.patch create mode 100644 fix-novendorchange-option-284.patch create mode 100644 path-replace-functools.wraps-with-six.wraps-bsc-1177.patch diff --git a/_lastrevision b/_lastrevision index b22e101..1f57267 100644 --- a/_lastrevision +++ b/_lastrevision @@ -1 +1 @@ -3ce95a1b386927b6f8cb27d1a6421018bebccd9a \ No newline at end of file +620b5f043062d74c4955be7609dc855696c0ca05 \ No newline at end of file diff --git a/fix-cve-2020-25592-and-add-tests-bsc-1178319.patch b/fix-cve-2020-25592-and-add-tests-bsc-1178319.patch new file mode 100644 index 0000000..940eb44 --- /dev/null +++ b/fix-cve-2020-25592-and-add-tests-bsc-1178319.patch @@ -0,0 +1,581 @@ +From e7514afcba4f57c5cb8599f561fcefdcc3db7314 Mon Sep 17 00:00:00 2001 +From: "Daniel A. Wozniak" +Date: Wed, 16 Sep 2020 00:25:10 +0000 +Subject: [PATCH] Fix CVE-2020-25592 and add tests (bsc#1178319) + +Properly validate eauth credentials and tokens on SSH calls made by Salt API + +(bsc#1178319) (bsc#1178362) (bsc#1178361) (CVE-2020-25592) (CVE-2020-17490) (CVE-2020-16846) +--- + salt/client/ssh/shell.py | 26 ++- + salt/modules/tls.py | 18 +- + salt/netapi/__init__.py | 67 ++++++ + tests/integration/netapi/test_client.py | 296 +++++++++++++++++++++++- + 4 files changed, 388 insertions(+), 19 deletions(-) + +diff --git a/salt/client/ssh/shell.py b/salt/client/ssh/shell.py +index bd55c514ee..27aba7b382 100644 +--- a/salt/client/ssh/shell.py ++++ b/salt/client/ssh/shell.py +@@ -8,6 +8,7 @@ from __future__ import absolute_import, print_function, unicode_literals + import re + import os + import sys ++import shlex + import time + import logging + import subprocess +@@ -43,10 +44,10 @@ def gen_key(path): + ''' + Generate a key for use with salt-ssh + ''' +- cmd = 'ssh-keygen -P "" -f {0} -t rsa -q'.format(path) ++ cmd = ["ssh-keygen", "-P", '""', "-f", path, "-t", "rsa", "-q"] + if not os.path.isdir(os.path.dirname(path)): + os.makedirs(os.path.dirname(path)) +- subprocess.call(cmd, shell=True) ++ subprocess.call(cmd) + + + def gen_shell(opts, **kwargs): +@@ -289,8 +290,7 @@ class Shell(object): + ''' + try: + proc = salt.utils.nb_popen.NonBlockingPopen( +- cmd, +- shell=True, ++ self._split_cmd(cmd), + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + ) +@@ -369,6 +369,21 @@ class Shell(object): + + return self._run_cmd(cmd) + ++ def _split_cmd(self, cmd): ++ """ ++ Split a command string so that it is suitable to pass to Popen without ++ shell=True. This prevents shell injection attacks in the options passed ++ to ssh or some other command. ++ """ ++ try: ++ ssh_part, cmd_part = cmd.split("/bin/sh") ++ except ValueError: ++ cmd_lst = shlex.split(cmd) ++ else: ++ cmd_lst = shlex.split(ssh_part) ++ cmd_lst.append("/bin/sh {}".format(cmd_part)) ++ return cmd_lst ++ + def _run_cmd(self, cmd, key_accept=False, passwd_retries=3): + ''' + Execute a shell command via VT. This is blocking and assumes that ssh +@@ -378,8 +393,7 @@ class Shell(object): + return '', 'No command or passphrase', 245 + + term = salt.utils.vt.Terminal( +- cmd, +- shell=True, ++ self._split_cmd(cmd), + log_stdout=True, + log_stdout_level='trace', + log_stderr=True, +diff --git a/salt/modules/tls.py b/salt/modules/tls.py +index af845621a3..116b5fe379 100644 +--- a/salt/modules/tls.py ++++ b/salt/modules/tls.py +@@ -798,12 +798,13 @@ def create_ca(ca_name, + if old_key.strip() == keycontent.strip(): + write_key = False + else: +- log.info('Saving old CA ssl key in %s', bck) +- with salt.utils.files.fopen(bck, 'w') as bckf: ++ log.info('Saving old CA ssl key in {0}'.format(bck)) ++ fp = os.open(bck, os.O_CREAT | os.O_RDWR, 0o600) ++ with os.fdopen(fp, 'w') as bckf: + bckf.write(old_key) +- os.chmod(bck, 0o600) + if write_key: +- with salt.utils.files.fopen(ca_keyp, 'wb') as ca_key: ++ fp = os.open(ca_keyp, os.O_CREAT | os.O_RDWR, 0o600) ++ with os.fdopen(fp, 'wb') as ca_key: + ca_key.write(salt.utils.stringutils.to_bytes(keycontent)) + + with salt.utils.files.fopen(certp, 'wb') as ca_crt: +@@ -1115,9 +1116,9 @@ def create_csr(ca_name, + req.sign(key, salt.utils.stringutils.to_str(digest)) + + # Write private key and request +- with salt.utils.files.fopen('{0}/{1}.key'.format(csr_path, +- csr_filename), +- 'wb+') as priv_key: ++ priv_keyp = '{0}/{1}.key'.format(csr_path, csr_filename) ++ fp = os.open(priv_keyp, os.O_CREAT | os.O_RDWR, 0o600) ++ with os.fdopen(fp, 'wb+') as priv_key: + priv_key.write( + salt.utils.stringutils.to_bytes( + OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, +@@ -1266,7 +1267,8 @@ def create_self_signed_cert(tls_dir='tls', + priv_key_path = '{0}/{1}/certs/{2}.key'.format(cert_base_path(), + tls_dir, + cert_filename) +- with salt.utils.files.fopen(priv_key_path, 'wb+') as priv_key: ++ fp = os.open(priv_key_path, os.O_CREAT | os.O_RDWR, 0o600) ++ with os.fdopen(fp, 'wb+') as priv_key: + priv_key.write( + salt.utils.stringutils.to_bytes( + OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, +diff --git a/salt/netapi/__init__.py b/salt/netapi/__init__.py +index 31a24bb420..4e5b6b093a 100644 +--- a/salt/netapi/__init__.py ++++ b/salt/netapi/__init__.py +@@ -3,24 +3,36 @@ + Make api awesomeness + ''' + from __future__ import absolute_import, print_function, unicode_literals ++ ++import copy ++ + # Import Python libs + import inspect ++import logging + import os + + # Import Salt libs + import salt.log # pylint: disable=W0611 ++import salt.auth + import salt.client + import salt.config ++import salt.daemons.masterapi + import salt.runner + import salt.syspaths + import salt.wheel + import salt.utils.args + import salt.client.ssh.client + import salt.exceptions ++import salt.utils.args ++import salt.utils.minions ++import salt.wheel ++from salt.defaults import DEFAULT_TARGET_DELIM + + # Import third party libs + from salt.ext import six + ++log = logging.getLogger(__name__) ++ + + class NetapiClient(object): + ''' +@@ -34,6 +46,15 @@ class NetapiClient(object): + + def __init__(self, opts): + self.opts = opts ++ apiopts = copy.deepcopy(self.opts) ++ apiopts["enable_ssh_minions"] = True ++ apiopts["cachedir"] = os.path.join(opts["cachedir"], "saltapi") ++ if not os.path.exists(apiopts["cachedir"]): ++ os.makedirs(apiopts["cachedir"]) ++ self.resolver = salt.auth.Resolver(apiopts) ++ self.loadauth = salt.auth.LoadAuth(apiopts) ++ self.key = salt.daemons.masterapi.access_keys(apiopts) ++ self.ckminions = salt.utils.minions.CkMinions(apiopts) + + def _is_master_running(self): + ''' +@@ -55,6 +76,49 @@ class NetapiClient(object): + self.opts['sock_dir'], + ipc_file)) + ++ def _prep_auth_info(self, clear_load): ++ sensitive_load_keys = [] ++ key = None ++ if "token" in clear_load: ++ auth_type = "token" ++ err_name = "TokenAuthenticationError" ++ sensitive_load_keys = ["token"] ++ return auth_type, err_name, key, sensitive_load_keys ++ elif "eauth" in clear_load: ++ auth_type = "eauth" ++ err_name = "EauthAuthenticationError" ++ sensitive_load_keys = ["username", "password"] ++ return auth_type, err_name, key, sensitive_load_keys ++ raise salt.exceptions.EauthAuthenticationError( ++ "No authentication credentials given" ++ ) ++ ++ def _authorize_ssh(self, low): ++ auth_type, err_name, key, sensitive_load_keys = self._prep_auth_info(low) ++ auth_check = self.loadauth.check_authentication(low, auth_type, key=key) ++ auth_list = auth_check.get("auth_list", []) ++ error = auth_check.get("error") ++ if error: ++ raise salt.exceptions.EauthAuthenticationError(error) ++ delimiter = low.get("kwargs", {}).get("delimiter", DEFAULT_TARGET_DELIM) ++ _res = self.ckminions.check_minions( ++ low["tgt"], low.get("tgt_type", "glob"), delimiter ++ ) ++ minions = _res.get("minions", list()) ++ missing = _res.get("missing", list()) ++ authorized = self.ckminions.auth_check( ++ auth_list, ++ low["fun"], ++ low.get("arg", []), ++ low["tgt"], ++ low.get("tgt_type", "glob"), ++ minions=minions, ++ ) ++ if not authorized: ++ raise salt.exceptions.EauthAuthenticationError( ++ "Authorization error occurred." ++ ) ++ + def run(self, low): + ''' + Execute the specified function in the specified client by passing the +@@ -80,6 +144,9 @@ class NetapiClient(object): + raise salt.exceptions.EauthAuthenticationError( + 'Raw shell option not allowed.') + ++ if low['client'] == 'ssh': ++ self._authorize_ssh(low) ++ + l_fun = getattr(self, low['client']) + f_call = salt.utils.args.format_call(l_fun, low) + return l_fun(*f_call.get('args', ()), **f_call.get('kwargs', {})) +diff --git a/tests/integration/netapi/test_client.py b/tests/integration/netapi/test_client.py +index 08030f31ec..b99bdfe313 100644 +--- a/tests/integration/netapi/test_client.py ++++ b/tests/integration/netapi/test_client.py +@@ -1,26 +1,30 @@ + # encoding: utf-8 +- + # Import Python libs + from __future__ import absolute_import, print_function, unicode_literals ++import copy + import logging + import os + import time + ++import salt.config ++import salt.netapi ++import salt.utils.files ++import salt.utils.platform ++import salt.utils.pycrypto ++ + # Import Salt Testing libs + from tests.support.paths import TMP_CONF_DIR, TMP + from tests.support.runtests import RUNTIME_VARS + from tests.support.unit import TestCase, skipIf + from tests.support.mock import patch +-from tests.support.case import SSHCase ++from tests.support.case import ModuleCase, SSHCase ++from salt.exceptions import EauthAuthenticationError + from tests.support.helpers import ( + Webserver, + SaveRequestsPostHandler, + requires_sshd_server + ) + +-# Import Salt libs +-import salt.config +-import salt.netapi + + from salt.exceptions import ( + EauthAuthenticationError +@@ -174,6 +178,10 @@ class NetapiSSHClientTest(SSHCase): + ''' + opts = salt.config.client_config(os.path.join(TMP_CONF_DIR, 'master')) + self.netapi = salt.netapi.NetapiClient(opts) ++ opts = salt.config.client_config(os.path.join(TMP_CONF_DIR, "master")) ++ naopts = copy.deepcopy(opts) ++ naopts["ignore_host_keys"] = True ++ self.netapi = salt.netapi.NetapiClient(naopts) + + self.priv_file = os.path.join(RUNTIME_VARS.TMP_CONF_DIR, 'key_test') + self.rosters = os.path.join(RUNTIME_VARS.TMP_CONF_DIR) +@@ -271,3 +279,281 @@ class NetapiSSHClientTest(SSHCase): + + self.assertEqual(ret, None) + self.assertFalse(os.path.exists('badfile.txt')) ++ ++ @staticmethod ++ def cleanup_file(path): ++ try: ++ os.remove(path) ++ except OSError: ++ pass ++ ++ @staticmethod ++ def cleanup_dir(path): ++ try: ++ salt.utils.files.rm_rf(path) ++ except OSError: ++ pass ++ ++ def test_shell_inject_ssh_priv(self): ++ """ ++ Verify CVE-2020-16846 for ssh_priv variable ++ """ ++ # ZDI-CAN-11143 ++ path = "/tmp/test-11143" ++ self.addCleanup(self.cleanup_file, path) ++ self.addCleanup(self.cleanup_file, "aaa") ++ self.addCleanup(self.cleanup_file, "aaa.pub") ++ self.addCleanup(self.cleanup_dir, "aaa|id>") ++ low = { ++ "roster": "cache", ++ "client": "ssh", ++ "tgt": "www.zerodayinitiative.com", ++ "ssh_priv": "aaa|id>{} #".format(path), ++ "fun": "test.ping", ++ "eauth": "auto", ++ "username": "saltdev_auto", ++ "password": "saltdev", ++ } ++ ret = self.netapi.run(low) ++ self.assertFalse(os.path.exists(path)) ++ ++ def test_shell_inject_tgt(self): ++ """ ++ Verify CVE-2020-16846 for tgt variable ++ """ ++ # ZDI-CAN-11167 ++ path = "/tmp/test-11167" ++ self.addCleanup(self.cleanup_file, path) ++ low = { ++ "roster": "cache", ++ "client": "ssh", ++ "tgt": "root|id>{} #@127.0.0.1".format(path), ++ "roster_file": "/tmp/salt-tests-tmpdir/config/roaster", ++ "rosters": "/", ++ "fun": "test.ping", ++ "eauth": "auto", ++ "username": "saltdev_auto", ++ "password": "saltdev", ++ } ++ ret = self.netapi.run(low) ++ self.assertFalse(os.path.exists(path)) ++ ++ def test_shell_inject_ssh_options(self): ++ """ ++ Verify CVE-2020-16846 for ssh_options ++ """ ++ # ZDI-CAN-11169 ++ path = "/tmp/test-11169" ++ self.addCleanup(self.cleanup_file, path) ++ low = { ++ "roster": "cache", ++ "client": "ssh", ++ "tgt": "127.0.0.1", ++ "renderer": "cheetah", ++ "fun": "test.ping", ++ "eauth": "auto", ++ "username": "saltdev_auto", ++ "password": "saltdev", ++ "roster_file": "/tmp/salt-tests-tmpdir/config/roaster", ++ "rosters": "/", ++ "ssh_options": ["|id>{} #".format(path), "lol"], ++ } ++ ret = self.netapi.run(low) ++ self.assertFalse(os.path.exists(path)) ++ ++ def test_shell_inject_ssh_port(self): ++ """ ++ Verify CVE-2020-16846 for ssh_port variable ++ """ ++ # ZDI-CAN-11172 ++ path = "/tmp/test-11172" ++ self.addCleanup(self.cleanup_file, path) ++ low = { ++ "roster": "cache", ++ "client": "ssh", ++ "tgt": "127.0.0.1", ++ "renderer": "cheetah", ++ "fun": "test.ping", ++ "eauth": "auto", ++ "username": "saltdev_auto", ++ "password": "saltdev", ++ "roster_file": "/tmp/salt-tests-tmpdir/config/roaster", ++ "rosters": "/", ++ "ssh_port": "hhhhh|id>{} #".format(path), ++ } ++ ret = self.netapi.run(low) ++ self.assertFalse(os.path.exists(path)) ++ ++ def test_shell_inject_remote_port_forwards(self): ++ """ ++ Verify CVE-2020-16846 for remote_port_forwards variable ++ """ ++ # ZDI-CAN-11173 ++ path = "/tmp/test-1173" ++ self.addCleanup(self.cleanup_file, path) ++ low = { ++ "roster": "cache", ++ "client": "ssh", ++ "tgt": "127.0.0.1", ++ "renderer": "cheetah", ++ "fun": "test.ping", ++ "roster_file": "/tmp/salt-tests-tmpdir/config/roaster", ++ "rosters": "/", ++ "ssh_remote_port_forwards": "hhhhh|id>{} #, lol".format(path), ++ "eauth": "auto", ++ "username": "saltdev_auto", ++ "password": "saltdev", ++ } ++ ret = self.netapi.run(low) ++ self.assertFalse(os.path.exists(path)) ++ ++ ++@requires_sshd_server ++class NetapiSSHClientAuthTest(SSHCase): ++ ++ USERA = "saltdev" ++ USERA_PWD = "saltdev" ++ ++ def setUp(self): ++ """ ++ Set up a NetapiClient instance ++ """ ++ opts = salt.config.client_config(os.path.join(TMP_CONF_DIR, "master")) ++ naopts = copy.deepcopy(opts) ++ naopts["ignore_host_keys"] = True ++ self.netapi = salt.netapi.NetapiClient(naopts) ++ ++ self.priv_file = os.path.join(RUNTIME_VARS.TMP_CONF_DIR, "key_test") ++ self.rosters = os.path.join(RUNTIME_VARS.TMP_CONF_DIR) ++ # Initialize salt-ssh ++ self.run_function("test.ping") ++ self.mod_case = ModuleCase() ++ try: ++ add_user = self.mod_case.run_function( ++ "user.add", [self.USERA], createhome=False ++ ) ++ self.assertTrue(add_user) ++ if salt.utils.platform.is_darwin(): ++ hashed_password = self.USERA_PWD ++ else: ++ hashed_password = salt.utils.pycrypto.gen_hash(password=self.USERA_PWD) ++ add_pwd = self.mod_case.run_function( ++ "shadow.set_password", [self.USERA, hashed_password], ++ ) ++ self.assertTrue(add_pwd) ++ except AssertionError: ++ self.mod_case.run_function("user.delete", [self.USERA], remove=True) ++ self.skipTest("Could not add user or password, skipping test") ++ ++ def tearDown(self): ++ del self.netapi ++ self.mod_case.run_function("user.delete", [self.USERA], remove=True) ++ ++ @classmethod ++ def setUpClass(cls): ++ cls.post_webserver = Webserver(handler=SaveRequestsPostHandler) ++ cls.post_webserver.start() ++ cls.post_web_root = cls.post_webserver.web_root ++ cls.post_web_handler = cls.post_webserver.handler ++ ++ @classmethod ++ def tearDownClass(cls): ++ cls.post_webserver.stop() ++ del cls.post_webserver ++ ++ def test_ssh_auth_bypass(self): ++ """ ++ CVE-2020-25592 - Bogus eauth raises exception. ++ """ ++ low = { ++ "roster": "cache", ++ "client": "ssh", ++ "tgt": "127.0.0.1", ++ "renderer": "cheetah", ++ "fun": "test.ping", ++ "roster_file": "/tmp/salt-tests-tmpdir/config/roaster", ++ "rosters": "/", ++ "eauth": "xx", ++ } ++ with self.assertRaises(salt.exceptions.EauthAuthenticationError): ++ ret = self.netapi.run(low) ++ ++ def test_ssh_auth_valid(self): ++ """ ++ CVE-2020-25592 - Valid eauth works as expected. ++ """ ++ low = { ++ "client": "ssh", ++ "tgt": "localhost", ++ "fun": "test.ping", ++ "roster_file": "roster", ++ "rosters": [self.rosters], ++ "ssh_priv": self.priv_file, ++ "eauth": "pam", ++ "username": "saltdev", ++ "password": "saltdev", ++ } ++ ret = self.netapi.run(low) ++ assert "localhost" in ret ++ assert ret["localhost"]["return"] is True ++ ++ def test_ssh_auth_invalid(self): ++ """ ++ CVE-2020-25592 - Wrong password raises exception. ++ """ ++ low = { ++ "client": "ssh", ++ "tgt": "localhost", ++ "fun": "test.ping", ++ "roster_file": "roster", ++ "rosters": [self.rosters], ++ "ssh_priv": self.priv_file, ++ "eauth": "pam", ++ "username": "saltdev", ++ "password": "notvalidpassword", ++ } ++ with self.assertRaises(salt.exceptions.EauthAuthenticationError): ++ ret = self.netapi.run(low) ++ ++ def test_ssh_auth_invalid_acl(self): ++ """ ++ CVE-2020-25592 - Eauth ACL enforced. ++ """ ++ low = { ++ "client": "ssh", ++ "tgt": "localhost", ++ "fun": "at.at", ++ "args": ["12:05am", "echo foo"], ++ "roster_file": "roster", ++ "rosters": [self.rosters], ++ "ssh_priv": self.priv_file, ++ "eauth": "pam", ++ "username": "saltdev", ++ "password": "notvalidpassword", ++ } ++ with self.assertRaises(salt.exceptions.EauthAuthenticationError): ++ ret = self.netapi.run(low) ++ ++ def test_ssh_auth_token(self): ++ """ ++ CVE-2020-25592 - Eauth tokens work as expected. ++ """ ++ low = { ++ "eauth": "pam", ++ "username": "saltdev", ++ "password": "saltdev", ++ } ++ ret = self.netapi.loadauth.mk_token(low) ++ assert "token" in ret and ret["token"] ++ low = { ++ "client": "ssh", ++ "tgt": "localhost", ++ "fun": "test.ping", ++ "roster_file": "roster", ++ "rosters": [self.rosters], ++ "ssh_priv": self.priv_file, ++ "token": ret["token"], ++ } ++ ret = self.netapi.run(low) ++ assert "localhost" in ret ++ assert ret["localhost"]["return"] is True +-- +2.28.0 + + diff --git a/fix-novendorchange-option-284.patch b/fix-novendorchange-option-284.patch new file mode 100644 index 0000000..1733896 --- /dev/null +++ b/fix-novendorchange-option-284.patch @@ -0,0 +1,240 @@ +From f69c1178de003866af412e61e0146597974eec0d Mon Sep 17 00:00:00 2001 +From: Martin Seidl +Date: Tue, 27 Oct 2020 16:12:29 +0100 +Subject: [PATCH] Fix novendorchange option (#284) + +* Fixed novendorchange handling in zypperpkg + +* refactor handling of novendorchange and fix tests +--- + salt/modules/zypperpkg.py | 19 ++-- + tests/unit/modules/test_zypperpkg.py | 150 ++++++++++++++++++++++++--- + 2 files changed, 148 insertions(+), 21 deletions(-) + +diff --git a/salt/modules/zypperpkg.py b/salt/modules/zypperpkg.py +index ad11da4ad1..d84a6af6e0 100644 +--- a/salt/modules/zypperpkg.py ++++ b/salt/modules/zypperpkg.py +@@ -1617,7 +1617,7 @@ def upgrade(refresh=True, + dryrun=False, + dist_upgrade=False, + fromrepo=None, +- novendorchange=False, ++ novendorchange=True, + skip_verify=False, + no_recommends=False, + root=None, +@@ -1701,13 +1701,18 @@ def upgrade(refresh=True, + log.info('Targeting repos: %s', fromrepo) + + if dist_upgrade: +- if novendorchange: +- # TODO: Grains validation should be moved to Zypper class +- if __grains__['osrelease_info'][0] > 11: +- cmd_update.append('--no-allow-vendor-change') +- log.info('Disabling vendor changes') ++ # TODO: Grains validation should be moved to Zypper class ++ if __grains__["osrelease_info"][0] > 11: ++ if novendorchange: ++ cmd_update.append("--no-allow-vendor-change") ++ log.info("Disabling vendor changes") + else: +- log.warning('Disabling vendor changes is not supported on this Zypper version') ++ cmd_update.append("--allow-vendor-change") ++ log.info("Enabling vendor changes") ++ else: ++ log.warning( ++ "Enabling/Disabling vendor changes is not supported on this Zypper version" ++ ) + + if no_recommends: + cmd_update.append('--no-recommends') +diff --git a/tests/unit/modules/test_zypperpkg.py b/tests/unit/modules/test_zypperpkg.py +index a3d20f66d5..8cc84485b5 100644 +--- a/tests/unit/modules/test_zypperpkg.py ++++ b/tests/unit/modules/test_zypperpkg.py +@@ -480,7 +480,11 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): + with patch('salt.modules.zypperpkg.list_pkgs', MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.2"}])): + ret = zypper.upgrade(dist_upgrade=True) + self.assertDictEqual(ret, {"vim": {"old": "1.1", "new": "1.2"}}) +- zypper_mock.assert_any_call('dist-upgrade', '--auto-agree-with-licenses') ++ zypper_mock.assert_any_call( ++ "dist-upgrade", ++ "--auto-agree-with-licenses", ++ "--no-allow-vendor-change", ++ ) + + with patch('salt.modules.zypperpkg.list_pkgs', MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}])): + ret = zypper.upgrade(dist_upgrade=True, dryrun=True) +@@ -488,25 +492,138 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): + zypper_mock.assert_any_call('dist-upgrade', '--auto-agree-with-licenses', + '--dry-run', '--debug-solver') + +- with patch('salt.modules.zypperpkg.list_pkgs', MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}])): +- ret = zypper.upgrade(dist_upgrade=True, dryrun=True, +- fromrepo=["Dummy", "Dummy2"], novendorchange=True) +- zypper_mock.assert_any_call('dist-upgrade', '--auto-agree-with-licenses', '--dry-run', +- '--from', "Dummy", '--from', 'Dummy2', '--no-allow-vendor-change') +- zypper_mock.assert_any_call('dist-upgrade', '--auto-agree-with-licenses', '--dry-run', +- '--from', "Dummy", '--from', 'Dummy2', '--no-allow-vendor-change', +- '--debug-solver') +- + with patch('salt.modules.zypperpkg.list_pkgs', MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}])): + ret = zypper.upgrade(dist_upgrade=False, fromrepo=["Dummy", "Dummy2"], dryrun=False) + zypper_mock.assert_any_call('update', '--auto-agree-with-licenses', '--repo', "Dummy", '--repo', 'Dummy2') + + with patch('salt.modules.zypperpkg.list_pkgs', MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.2"}])): + ret = zypper.upgrade(dist_upgrade=True, fromrepo=["Dummy", "Dummy2"], novendorchange=True) ++ zypper_mock.assert_any_call( ++ "dist-upgrade", ++ "--auto-agree-with-licenses", ++ "--dry-run", ++ "--no-allow-vendor-change", ++ ) ++ zypper_mock.assert_any_call( ++ "dist-upgrade", ++ "--auto-agree-with-licenses", ++ "--dry-run", ++ "--no-allow-vendor-change", ++ ) ++ ++ with patch( ++ "salt.modules.zypperpkg.list_pkgs", ++ MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}]), ++ ): ++ ret = zypper.upgrade( ++ dist_upgrade=True, ++ dryrun=True, ++ fromrepo=["Dummy", "Dummy2"], ++ novendorchange=False, ++ ) ++ zypper_mock.assert_any_call( ++ "dist-upgrade", ++ "--auto-agree-with-licenses", ++ "--dry-run", ++ "--from", ++ "Dummy", ++ "--from", ++ "Dummy2", ++ "--allow-vendor-change", ++ ) ++ zypper_mock.assert_any_call( ++ "dist-upgrade", ++ "--auto-agree-with-licenses", ++ "--dry-run", ++ "--from", ++ "Dummy", ++ "--from", ++ "Dummy2", ++ "--allow-vendor-change", ++ "--debug-solver", ++ ) ++ ++ ++ with patch( ++ "salt.modules.zypperpkg.list_pkgs", ++ MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}]), ++ ): ++ ret = zypper.upgrade( ++ dist_upgrade=True, ++ dryrun=True, ++ fromrepo=["Dummy", "Dummy2"], ++ novendorchange=True, ++ ) ++ zypper_mock.assert_any_call( ++ "dist-upgrade", ++ "--auto-agree-with-licenses", ++ "--dry-run", ++ "--from", ++ "Dummy", ++ "--from", ++ "Dummy2", ++ "--no-allow-vendor-change", ++ ) ++ zypper_mock.assert_any_call( ++ "dist-upgrade", ++ "--auto-agree-with-licenses", ++ "--dry-run", ++ "--from", ++ "Dummy", ++ "--from", ++ "Dummy2", ++ "--no-allow-vendor-change", ++ "--debug-solver", ++ ) ++ ++ with patch( ++ "salt.modules.zypperpkg.list_pkgs", ++ MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}]), ++ ): ++ ret = zypper.upgrade( ++ dist_upgrade=False, fromrepo=["Dummy", "Dummy2"], dryrun=False ++ ) ++ zypper_mock.assert_any_call( ++ "update", ++ "--auto-agree-with-licenses", ++ "--repo", ++ "Dummy", ++ "--repo", ++ "Dummy2", ++ ) ++ ++ with patch( ++ "salt.modules.zypperpkg.list_pkgs", ++ MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.2"}]), ++ ): ++ ret = zypper.upgrade( ++ dist_upgrade=True, ++ fromrepo=["Dummy", "Dummy2"], ++ novendorchange=True, ++ ) + self.assertDictEqual(ret, {"vim": {"old": "1.1", "new": "1.2"}}) + zypper_mock.assert_any_call('dist-upgrade', '--auto-agree-with-licenses', '--from', "Dummy", + '--from', 'Dummy2', '--no-allow-vendor-change') + ++ with patch( ++ "salt.modules.zypperpkg.list_pkgs", ++ MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.2"}]), ++ ): ++ ret = zypper.upgrade( ++ dist_upgrade=True, ++ fromrepo=["Dummy", "Dummy2"], ++ novendorchange=False, ++ ) ++ self.assertDictEqual(ret, {"vim": {"old": "1.1", "new": "1.2"}}) ++ zypper_mock.assert_any_call( ++ "dist-upgrade", ++ "--auto-agree-with-licenses", ++ "--from", ++ "Dummy", ++ "--from", ++ "Dummy2", ++ "--allow-vendor-change", ++ ) + def test_upgrade_kernel(self): + ''' + Test kernel package upgrade success. +@@ -558,10 +675,15 @@ Repository 'DUMMY' not found by its alias, number, or URI. + with patch('salt.modules.zypperpkg.list_pkgs', MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}])): + with self.assertRaises(CommandExecutionError) as cmd_exc: + ret = zypper.upgrade(dist_upgrade=True, fromrepo=["DUMMY"]) +- self.assertEqual(cmd_exc.exception.info['changes'], {}) +- self.assertEqual(cmd_exc.exception.info['result']['stdout'], zypper_out) +- zypper_mock.noraise.call.assert_called_with('dist-upgrade', '--auto-agree-with-licenses', +- '--from', 'DUMMY') ++ self.assertEqual(cmd_exc.exception.info["changes"], {}) ++ self.assertEqual(cmd_exc.exception.info["result"]["stdout"], zypper_out) ++ zypper_mock.noraise.call.assert_called_with( ++ "dist-upgrade", ++ "--auto-agree-with-licenses", ++ "--from", ++ "DUMMY", ++ "--no-allow-vendor-change", ++ ) + + def test_upgrade_available(self): + ''' +-- +2.28.0 + + diff --git a/path-replace-functools.wraps-with-six.wraps-bsc-1177.patch b/path-replace-functools.wraps-with-six.wraps-bsc-1177.patch new file mode 100644 index 0000000..43d0476 --- /dev/null +++ b/path-replace-functools.wraps-with-six.wraps-bsc-1177.patch @@ -0,0 +1,54 @@ +From 9707eab7452a94e64f77ece707c31c37e43e47f2 Mon Sep 17 00:00:00 2001 +From: Alberto Planas +Date: Tue, 20 Oct 2020 13:13:24 +0200 +Subject: [PATCH] path: replace functools.wraps with six.wraps + (bsc#1177867) (#283) + +Python 2.7 functools.wraps decorator do not add the `__wrapped__` +attribute to decorated functions, that is used by Salt to access the +original function and deduce the parameters from the signature. + +This patch uses six.wraps, that add this extra attribute. +--- + salt/utils/decorators/path.py | 7 +++---- + 1 file changed, 3 insertions(+), 4 deletions(-) + +diff --git a/salt/utils/decorators/path.py b/salt/utils/decorators/path.py +index 8ee7fb1d11..a763c92d22 100644 +--- a/salt/utils/decorators/path.py ++++ b/salt/utils/decorators/path.py +@@ -4,11 +4,10 @@ Decorators for salt.utils.path + ''' + from __future__ import absolute_import, print_function, unicode_literals + +-import functools +- + # Import Salt libs + import salt.utils.path + from salt.exceptions import CommandNotFoundError ++import salt.ext.six + + + def which(exe): +@@ -16,7 +15,7 @@ def which(exe): + Decorator wrapper for salt.utils.path.which + ''' + def wrapper(function): +- @functools.wraps(function) ++ @salt.ext.six.wraps(function) + def wrapped(*args, **kwargs): + if salt.utils.path.which(exe) is None: + raise CommandNotFoundError( +@@ -34,7 +33,7 @@ def which_bin(exes): + Decorator wrapper for salt.utils.path.which_bin + ''' + def wrapper(function): +- @functools.wraps(function) ++ @salt.ext.six.wraps(function) + def wrapped(*args, **kwargs): + if salt.utils.path.which_bin(exes) is None: + raise CommandNotFoundError( +-- +2.28.0 + + diff --git a/salt.changes b/salt.changes index 3dff44d..961bf41 100644 --- a/salt.changes +++ b/salt.changes @@ -1,3 +1,29 @@ +------------------------------------------------------------------- +Wed Nov 4 10:54:32 UTC 2020 - Pablo Suárez Hernández + +- Properly validate eauth credentials and tokens on SSH calls made by Salt API + (bsc#1178319) (bsc#1178362) (bsc#1178361) + (CVE-2020-25592) (CVE-2020-17490) (CVE-2020-16846) + +- Added: + * fix-cve-2020-25592-and-add-tests-bsc-1178319.patch + +------------------------------------------------------------------- +Tue Oct 27 15:23:12 UTC 2020 - Pablo Suárez Hernández + +- Fix novendorchange handling in zypperpkg module + +- Added: + * fix-novendorchange-option-284.patch + +------------------------------------------------------------------- +Tue Oct 20 11:43:49 UTC 2020 - Pablo Suárez Hernández + +- Fix disk.blkid to avoid unexpected keyword argument '__pub_user' (bsc#1177867) + +- Added: + * path-replace-functools.wraps-with-six.wraps-bsc-1177.patch + ------------------------------------------------------------------- Wed Oct 14 10:49:33 UTC 2020 - Pablo Suárez Hernández diff --git a/salt.spec b/salt.spec index a5ac381..c426c77 100644 --- a/salt.spec +++ b/salt.spec @@ -363,6 +363,12 @@ Patch138: fix-grains.test_core-unit-test-277.patch Patch139: drop-wrong-mock-from-chroot-unit-test.patch # PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/280 Patch140: ensure-virt.update-stop_on_reboot-is-updated-with-it.patch +# PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/281 +Patch141: path-replace-functools.wraps-with-six.wraps-bsc-1177.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/58560 +Patch142: fix-novendorchange-option-284.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/58871 +Patch143: fix-cve-2020-25592-and-add-tests-bsc-1178319.patch BuildRoot: %{_tmppath}/%{name}-%{version}-build BuildRequires: logrotate @@ -1010,6 +1016,9 @@ cp %{S:5} ./.travis.yml %patch138 -p1 %patch139 -p1 %patch140 -p1 +%patch141 -p1 +%patch142 -p1 +%patch143 -p1 %build # Putting /usr/bin at the front of $PATH is needed for RHEL/RES 7. Without this From 24e47d966b26631fa9e84ed39fad83d496c6952be8b7b25fff6834a50557e738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= Date: Fri, 6 Nov 2020 10:10:40 +0000 Subject: [PATCH 2/2] osc copypac from project:systemsmanagement:saltstack:testing package:salt revision:365 OBS-URL: https://build.opensuse.org/package/show/systemsmanagement:saltstack/salt?expand=0&rev=177 --- _lastrevision | 2 +- salt.changes | 8 +++++ salt.spec | 3 ++ ...or-salt-ssh-keys-to-empty-string-293.patch | 31 +++++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 set-passphrase-for-salt-ssh-keys-to-empty-string-293.patch diff --git a/_lastrevision b/_lastrevision index 1f57267..1f5953f 100644 --- a/_lastrevision +++ b/_lastrevision @@ -1 +1 @@ -620b5f043062d74c4955be7609dc855696c0ca05 \ No newline at end of file +24bd64b440c2c3f0f154a1b7f7216de20dc07df8 \ No newline at end of file diff --git a/salt.changes b/salt.changes index 961bf41..1d28930 100644 --- a/salt.changes +++ b/salt.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Fri Nov 6 09:19:22 UTC 2020 - Pablo Suárez Hernández + +- Set passphrase for salt-ssh keys to empty string (bsc#1178485) + +- Added: + * set-passphrase-for-salt-ssh-keys-to-empty-string-293.patch + ------------------------------------------------------------------- Wed Nov 4 10:54:32 UTC 2020 - Pablo Suárez Hernández diff --git a/salt.spec b/salt.spec index c426c77..8a28880 100644 --- a/salt.spec +++ b/salt.spec @@ -369,6 +369,8 @@ Patch141: path-replace-functools.wraps-with-six.wraps-bsc-1177.patch Patch142: fix-novendorchange-option-284.patch # PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/58871 Patch143: fix-cve-2020-25592-and-add-tests-bsc-1178319.patch +# PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/293 +Patch144: set-passphrase-for-salt-ssh-keys-to-empty-string-293.patch BuildRoot: %{_tmppath}/%{name}-%{version}-build BuildRequires: logrotate @@ -1019,6 +1021,7 @@ cp %{S:5} ./.travis.yml %patch141 -p1 %patch142 -p1 %patch143 -p1 +%patch144 -p1 %build # Putting /usr/bin at the front of $PATH is needed for RHEL/RES 7. Without this diff --git a/set-passphrase-for-salt-ssh-keys-to-empty-string-293.patch b/set-passphrase-for-salt-ssh-keys-to-empty-string-293.patch new file mode 100644 index 0000000..884eb1a --- /dev/null +++ b/set-passphrase-for-salt-ssh-keys-to-empty-string-293.patch @@ -0,0 +1,31 @@ +From 677b7a8881a2e9ebab58cead29b1a6d83850c888 Mon Sep 17 00:00:00 2001 +From: Alexander Graul +Date: Thu, 5 Nov 2020 16:54:44 +0100 +Subject: [PATCH] Set passphrase for salt-ssh keys to empty string + (#293) + +Since the cmd is not passed to a shell anymore, the "" are taken +literally and not as an empty string. + +Bugzilla report: https://bugzilla.suse.com/show_bug.cgi?id=1178485 +--- + salt/client/ssh/shell.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/salt/client/ssh/shell.py b/salt/client/ssh/shell.py +index 27aba7b382..27ab9f4f1b 100644 +--- a/salt/client/ssh/shell.py ++++ b/salt/client/ssh/shell.py +@@ -44,7 +44,7 @@ def gen_key(path): + ''' + Generate a key for use with salt-ssh + ''' +- cmd = ["ssh-keygen", "-P", '""', "-f", path, "-t", "rsa", "-q"] ++ cmd = ["ssh-keygen", "-P", "", "-f", path, "-t", "rsa", "-q"] + if not os.path.isdir(os.path.dirname(path)): + os.makedirs(os.path.dirname(path)) + subprocess.call(cmd) +-- +2.28.0 + +