We have some news to share for the request index beta feature. We’ve added more options to sort your requests, counters to the individual filters and documentation for the search functionality. Checkout the blog post for more details.

File fix-cve-2024-22231-and-cve-2024-22232-bsc-1219430-bs.patch of Package venv-salt-minion

From 5710bc3ff3887762182f8326bd74f40d3872a69f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?=
 <psuarezhernandez@suse.com>
Date: Thu, 1 Feb 2024 11:50:16 +0000
Subject: [PATCH] Fix "CVE-2024-22231" and "CVE-2024-22232"
 (bsc#1219430, bsc#1219431) (#621)

* Fix CVE-2024-22231 and CVE-2024-22232

* Add changelogs for CVE-2024-22231 and CVE-2024-22232

* Fix linter issue

* Add credit

* Fix wart in patch

* Clean up test fixtures

* Fix test on windows

* Update changelog file name

* Fix fileroots tests

---------

Co-authored-by: Daniel A. Wozniak <dwozniak@vmware.com>
---
 changelog/565.security.md                   |   4 +
 salt/fileserver/__init__.py                 |   9 +-
 salt/fileserver/roots.py                    |  26 +++++
 salt/master.py                              |  15 ++-
 tests/pytests/unit/fileserver/test_roots.py |  58 +++++++--
 tests/pytests/unit/test_fileserver.py       | 123 ++++++++++++++++++++
 tests/pytests/unit/test_master.py           |  33 ++++++
 tests/unit/test_fileserver.py               |  79 -------------
 8 files changed, 250 insertions(+), 97 deletions(-)
 create mode 100644 changelog/565.security.md
 create mode 100644 tests/pytests/unit/test_fileserver.py
 delete mode 100644 tests/unit/test_fileserver.py

diff --git a/changelog/565.security.md b/changelog/565.security.md
new file mode 100644
index 00000000000..5d7ec8202ba
--- /dev/null
+++ b/changelog/565.security.md
@@ -0,0 +1,4 @@
+CVE-2024-22231 Prevent directory traversal when creating syndic cache directory on the master
+CVE-2024-22232 Prevent directory traversal attacks in the master's serve_file method.
+These vulerablities were discovered and reported by:
+Yudi Zhao(Huawei Nebula Security Lab),Chenwei Jiang(Huawei Nebula Security Lab)
diff --git a/salt/fileserver/__init__.py b/salt/fileserver/__init__.py
index 99f12387f91..4eca98d14a4 100644
--- a/salt/fileserver/__init__.py
+++ b/salt/fileserver/__init__.py
@@ -568,11 +568,6 @@ class Fileserver:
         saltenv = salt.utils.stringutils.to_unicode(saltenv)
         back = self.backends(back)
         kwargs = {}
-        fnd = {"path": "", "rel": ""}
-        if os.path.isabs(path):
-            return fnd
-        if "../" in path:
-            return fnd
         if salt.utils.url.is_escaped(path):
             # don't attempt to find URL query arguments in the path
             path = salt.utils.url.unescape(path)
@@ -588,6 +583,10 @@ class Fileserver:
                     args = comp.split("=", 1)
                     kwargs[args[0]] = args[1]
 
+        fnd = {"path": "", "rel": ""}
+        if os.path.isabs(path) or "../" in path:
+            return fnd
+
         if "env" in kwargs:
             # "env" is not supported; Use "saltenv".
             kwargs.pop("env")
diff --git a/salt/fileserver/roots.py b/salt/fileserver/roots.py
index a02b597c6f8..e2ea92029c3 100644
--- a/salt/fileserver/roots.py
+++ b/salt/fileserver/roots.py
@@ -27,6 +27,7 @@ import salt.utils.hashutils
 import salt.utils.path
 import salt.utils.platform
 import salt.utils.stringutils
+import salt.utils.verify
 import salt.utils.versions
 
 log = logging.getLogger(__name__)
@@ -98,6 +99,11 @@ def find_file(path, saltenv="base", **kwargs):
         if saltenv == "__env__":
             root = root.replace("__env__", actual_saltenv)
         full = os.path.join(root, path)
+
+        # Refuse to serve file that is not under the root.
+        if not salt.utils.verify.clean_path(root, full, subdir=True):
+            continue
+
         if os.path.isfile(full) and not salt.fileserver.is_file_ignored(__opts__, full):
             fnd["path"] = full
             fnd["rel"] = path
@@ -128,6 +134,26 @@ def serve_file(load, fnd):
     ret["dest"] = fnd["rel"]
     gzip = load.get("gzip", None)
     fpath = os.path.normpath(fnd["path"])
+
+    actual_saltenv = saltenv = load["saltenv"]
+    if saltenv not in __opts__["file_roots"]:
+        if "__env__" in __opts__["file_roots"]:
+            log.debug(
+                "salt environment '%s' maps to __env__ file_roots directory", saltenv
+            )
+            saltenv = "__env__"
+        else:
+            return fnd
+    file_in_root = False
+    for root in __opts__["file_roots"][saltenv]:
+        if saltenv == "__env__":
+            root = root.replace("__env__", actual_saltenv)
+        # Refuse to serve file that is not under the root.
+        if salt.utils.verify.clean_path(root, fpath, subdir=True):
+            file_in_root = True
+    if not file_in_root:
+        return ret
+
     with salt.utils.files.fopen(fpath, "rb") as fp_:
         fp_.seek(load["loc"])
         data = fp_.read(__opts__["file_buffer_size"])
diff --git a/salt/master.py b/salt/master.py
index 3d2ba1e29de..425b4121481 100644
--- a/salt/master.py
+++ b/salt/master.py
@@ -1038,7 +1038,10 @@ class MWorker(salt.utils.process.SignalHandlingProcess):
         """
         key = payload["enc"]
         load = payload["load"]
-        ret = {"aes": self._handle_aes, "clear": self._handle_clear}[key](load)
+        if key == "aes":
+            ret = self._handle_aes(load)
+        else:
+            ret = self._handle_clear(load)
         raise salt.ext.tornado.gen.Return(ret)
 
     def _post_stats(self, start, cmd):
@@ -1213,7 +1216,7 @@ class AESFuncs(TransportMethods):
         "_dir_list",
         "_symlink_list",
         "_file_envs",
-        "_ext_nodes", # To keep compatibility with old Salt minion versions
+        "_ext_nodes",  # To keep compatibility with old Salt minion versions
     )
 
     def __init__(self, opts, context=None):
@@ -1746,10 +1749,16 @@ class AESFuncs(TransportMethods):
                 self.mminion.returners[fstr](load["jid"], load["load"])
 
             # Register the syndic
+
+            # We are creating a path using user suplied input. Use the
+            # clean_path to prevent a directory traversal.
+            root = os.path.join(self.opts["cachedir"], "syndics")
             syndic_cache_path = os.path.join(
                 self.opts["cachedir"], "syndics", load["id"]
             )
-            if not os.path.exists(syndic_cache_path):
+            if salt.utils.verify.clean_path(
+                root, syndic_cache_path
+            ) and not os.path.exists(syndic_cache_path):
                 path_name = os.path.split(syndic_cache_path)[0]
                 if not os.path.exists(path_name):
                     os.makedirs(path_name)
diff --git a/tests/pytests/unit/fileserver/test_roots.py b/tests/pytests/unit/fileserver/test_roots.py
index 96bceb0fd3d..c1660280bc5 100644
--- a/tests/pytests/unit/fileserver/test_roots.py
+++ b/tests/pytests/unit/fileserver/test_roots.py
@@ -5,6 +5,7 @@
 import copy
 import pathlib
 import shutil
+import sys
 import textwrap
 
 import pytest
@@ -28,14 +29,14 @@ def unicode_dirname():
     return "соль"
 
 
-@pytest.fixture(autouse=True)
+@pytest.fixture
 def testfile(tmp_path):
     fp = tmp_path / "testfile"
     fp.write_text("This is a testfile")
     return fp
 
 
-@pytest.fixture(autouse=True)
+@pytest.fixture
 def tmp_state_tree(tmp_path, testfile, unicode_filename, unicode_dirname):
     dirname = tmp_path / "roots_tmp_state_tree"
     dirname.mkdir(parents=True, exist_ok=True)
@@ -54,11 +55,15 @@ def tmp_state_tree(tmp_path, testfile, unicode_filename, unicode_dirname):
 
 
 @pytest.fixture
-def configure_loader_modules(tmp_state_tree, temp_salt_master):
-    opts = temp_salt_master.config.copy()
+def testfilepath(tmp_state_tree, testfile):
+    return tmp_state_tree / testfile.name
+
+
+@pytest.fixture
+def configure_loader_modules(tmp_state_tree, master_opts):
     overrides = {"file_roots": {"base": [str(tmp_state_tree)]}}
-    opts.update(overrides)
-    return {roots: {"__opts__": opts}}
+    master_opts.update(overrides)
+    return {roots: {"__opts__": master_opts}}
 
 
 def test_file_list(unicode_filename):
@@ -75,17 +80,17 @@ def test_find_file(tmp_state_tree):
     assert full_path_to_file == ret["path"]
 
 
-def test_serve_file(testfile):
+def test_serve_file(testfilepath):
     with patch.dict(roots.__opts__, {"file_buffer_size": 262144}):
         load = {
             "saltenv": "base",
-            "path": str(testfile),
+            "path": str(testfilepath),
             "loc": 0,
         }
-        fnd = {"path": str(testfile), "rel": "testfile"}
+        fnd = {"path": str(testfilepath), "rel": "testfile"}
         ret = roots.serve_file(load, fnd)
 
-        with salt.utils.files.fopen(str(testfile), "rb") as fp_:
+        with salt.utils.files.fopen(str(testfilepath), "rb") as fp_:
             data = fp_.read()
 
         assert ret == {"data": data, "dest": "testfile"}
@@ -277,3 +282,36 @@ def test_update_mtime_map_unicode_error(tmp_path):
         },
         "backend": "roots",
     }
+
+
+def test_find_file_not_in_root(tmp_state_tree):
+    """
+    Fileroots should never 'find' a file that is outside of it's root.
+    """
+    badfile = pathlib.Path(tmp_state_tree).parent / "bar"
+    badfile.write_text("Bad file")
+    badpath = f"../bar"
+    ret = roots.find_file(badpath)
+    assert ret == {"path": "", "rel": ""}
+    badpath = f"{tmp_state_tree / '..' / 'bar'}"
+    ret = roots.find_file(badpath)
+    assert ret == {"path": "", "rel": ""}
+
+
+def test_serve_file_not_in_root(tmp_state_tree):
+    """
+    Fileroots should never 'serve' a file that is outside of it's root.
+    """
+    badfile = pathlib.Path(tmp_state_tree).parent / "bar"
+    badfile.write_text("Bad file")
+    badpath = f"../bar"
+    load = {"path": "salt://|..\\bar", "saltenv": "base", "loc": 0}
+    fnd = {
+        "path": f"{tmp_state_tree / '..' / 'bar'}",
+        "rel": f"{pathlib.Path('..') / 'bar'}",
+    }
+    ret = roots.serve_file(load, fnd)
+    if "win" in sys.platform:
+        assert ret == {"data": "", "dest": "..\\bar"}
+    else:
+        assert ret == {"data": "", "dest": "../bar"}
diff --git a/tests/pytests/unit/test_fileserver.py b/tests/pytests/unit/test_fileserver.py
new file mode 100644
index 00000000000..8dd3ea0a27d
--- /dev/null
+++ b/tests/pytests/unit/test_fileserver.py
@@ -0,0 +1,123 @@
+import datetime
+import os
+import time
+
+import salt.fileserver
+import salt.utils.files
+
+
+def test_diff_with_diffent_keys():
+    """
+    Test that different maps are indeed reported different
+    """
+    map1 = {"file1": 1234}
+    map2 = {"file2": 1234}
+    assert salt.fileserver.diff_mtime_map(map1, map2) is True
+
+
+def test_diff_with_diffent_values():
+    """
+    Test that different maps are indeed reported different
+    """
+    map1 = {"file1": 12345}
+    map2 = {"file1": 1234}
+    assert salt.fileserver.diff_mtime_map(map1, map2) is True
+
+
+def test_whitelist():
+    opts = {
+        "fileserver_backend": ["roots", "git", "s3fs", "hgfs", "svn"],
+        "extension_modules": "",
+    }
+    fs = salt.fileserver.Fileserver(opts)
+    assert sorted(fs.servers.whitelist) == sorted(
+        ["git", "gitfs", "hg", "hgfs", "svn", "svnfs", "roots", "s3fs"]
+    ), fs.servers.whitelist
+
+
+def test_future_file_list_cache_file_ignored(tmp_path):
+    opts = {
+        "fileserver_backend": ["roots"],
+        "cachedir": tmp_path,
+        "extension_modules": "",
+    }
+
+    back_cachedir = os.path.join(tmp_path, "file_lists/roots")
+    os.makedirs(os.path.join(back_cachedir))
+
+    # Touch a couple files
+    for filename in ("base.p", "foo.txt"):
+        with salt.utils.files.fopen(os.path.join(back_cachedir, filename), "wb") as _f:
+            if filename == "base.p":
+                _f.write(b"\x80")
+
+    # Set modification time to file list cache file to 1 year in the future
+    now = datetime.datetime.utcnow()
+    future = now + datetime.timedelta(days=365)
+    mod_time = time.mktime(future.timetuple())
+    os.utime(os.path.join(back_cachedir, "base.p"), (mod_time, mod_time))
+
+    list_cache = os.path.join(back_cachedir, "base.p")
+    w_lock = os.path.join(back_cachedir, ".base.w")
+    ret = salt.fileserver.check_file_list_cache(opts, "files", list_cache, w_lock)
+    assert (
+        ret[1] is True
+    ), "Cache file list cache file is not refreshed when future modification time"
+
+
+def test_file_server_url_escape(tmp_path):
+    (tmp_path / "srv").mkdir()
+    (tmp_path / "srv" / "salt").mkdir()
+    (tmp_path / "foo").mkdir()
+    (tmp_path / "foo" / "bar").write_text("Bad file")
+    fileroot = str(tmp_path / "srv" / "salt")
+    badfile = str(tmp_path / "foo" / "bar")
+    opts = {
+        "fileserver_backend": ["roots"],
+        "extension_modules": "",
+        "optimization_order": [
+            0,
+        ],
+        "file_roots": {
+            "base": [fileroot],
+        },
+        "file_ignore_regex": "",
+        "file_ignore_glob": "",
+    }
+    fs = salt.fileserver.Fileserver(opts)
+    ret = fs.find_file(
+        "salt://|..\\..\\..\\foo/bar",
+        "base",
+    )
+    assert ret == {"path": "", "rel": ""}
+
+
+def test_file_server_serve_url_escape(tmp_path):
+    (tmp_path / "srv").mkdir()
+    (tmp_path / "srv" / "salt").mkdir()
+    (tmp_path / "foo").mkdir()
+    (tmp_path / "foo" / "bar").write_text("Bad file")
+    fileroot = str(tmp_path / "srv" / "salt")
+    badfile = str(tmp_path / "foo" / "bar")
+    opts = {
+        "fileserver_backend": ["roots"],
+        "extension_modules": "",
+        "optimization_order": [
+            0,
+        ],
+        "file_roots": {
+            "base": [fileroot],
+        },
+        "file_ignore_regex": "",
+        "file_ignore_glob": "",
+        "file_buffer_size": 2048,
+    }
+    fs = salt.fileserver.Fileserver(opts)
+    ret = fs.serve_file(
+        {
+            "path": "salt://|..\\..\\..\\foo/bar",
+            "saltenv": "base",
+            "loc": 0,
+        }
+    )
+    assert ret == {"data": "", "dest": ""}
diff --git a/tests/pytests/unit/test_master.py b/tests/pytests/unit/test_master.py
index 98c796912aa..d338307d1f8 100644
--- a/tests/pytests/unit/test_master.py
+++ b/tests/pytests/unit/test_master.py
@@ -1,3 +1,4 @@
+import pathlib
 import time
 
 import pytest
@@ -249,3 +250,35 @@ def test_mworker_pass_context():
                 loadler_pillars_mock.call_args_list[0][1].get("pack").get("__context__")
                 == test_context
             )
+
+
+def test_syndic_return_cache_dir_creation(encrypted_requests):
+    """master's cachedir for a syndic will be created by AESFuncs._syndic_return method"""
+    cachedir = pathlib.Path(encrypted_requests.opts["cachedir"])
+    assert not (cachedir / "syndics").exists()
+    encrypted_requests._syndic_return(
+        {
+            "id": "mamajama",
+            "jid": "",
+            "return": {},
+        }
+    )
+    assert (cachedir / "syndics").exists()
+    assert (cachedir / "syndics" / "mamajama").exists()
+
+
+def test_syndic_return_cache_dir_creation_traversal(encrypted_requests):
+    """
+    master's  AESFuncs._syndic_return method cachdir creation is not vulnerable to a directory traversal
+    """
+    cachedir = pathlib.Path(encrypted_requests.opts["cachedir"])
+    assert not (cachedir / "syndics").exists()
+    encrypted_requests._syndic_return(
+        {
+            "id": "../mamajama",
+            "jid": "",
+            "return": {},
+        }
+    )
+    assert not (cachedir / "syndics").exists()
+    assert not (cachedir / "mamajama").exists()
diff --git a/tests/unit/test_fileserver.py b/tests/unit/test_fileserver.py
deleted file mode 100644
index c290b16b7e4..00000000000
--- a/tests/unit/test_fileserver.py
+++ /dev/null
@@ -1,79 +0,0 @@
-"""
-    :codeauthor: Joao Mesquita <jmesquita@sangoma.com>
-"""
-
-
-import datetime
-import os
-import time
-
-import salt.utils.files
-from salt import fileserver
-from tests.support.helpers import with_tempdir
-from tests.support.mixins import LoaderModuleMockMixin
-from tests.support.unit import TestCase
-
-
-class MapDiffTestCase(TestCase):
-    def test_diff_with_diffent_keys(self):
-        """
-        Test that different maps are indeed reported different
-        """
-        map1 = {"file1": 1234}
-        map2 = {"file2": 1234}
-        assert fileserver.diff_mtime_map(map1, map2) is True
-
-    def test_diff_with_diffent_values(self):
-        """
-        Test that different maps are indeed reported different
-        """
-        map1 = {"file1": 12345}
-        map2 = {"file1": 1234}
-        assert fileserver.diff_mtime_map(map1, map2) is True
-
-
-class VCSBackendWhitelistCase(TestCase, LoaderModuleMockMixin):
-    def setup_loader_modules(self):
-        return {fileserver: {}}
-
-    def test_whitelist(self):
-        opts = {
-            "fileserver_backend": ["roots", "git", "s3fs", "hgfs", "svn"],
-            "extension_modules": "",
-        }
-        fs = fileserver.Fileserver(opts)
-        assert sorted(fs.servers.whitelist) == sorted(
-            ["git", "gitfs", "hg", "hgfs", "svn", "svnfs", "roots", "s3fs"]
-        ), fs.servers.whitelist
-
-    @with_tempdir()
-    def test_future_file_list_cache_file_ignored(self, cachedir):
-        opts = {
-            "fileserver_backend": ["roots"],
-            "cachedir": cachedir,
-            "extension_modules": "",
-        }
-
-        back_cachedir = os.path.join(cachedir, "file_lists/roots")
-        os.makedirs(os.path.join(back_cachedir))
-
-        # Touch a couple files
-        for filename in ("base.p", "foo.txt"):
-            with salt.utils.files.fopen(
-                os.path.join(back_cachedir, filename), "wb"
-            ) as _f:
-                if filename == "base.p":
-                    _f.write(b"\x80")
-
-        # Set modification time to file list cache file to 1 year in the future
-        now = datetime.datetime.utcnow()
-        future = now + datetime.timedelta(days=365)
-        mod_time = time.mktime(future.timetuple())
-        os.utime(os.path.join(back_cachedir, "base.p"), (mod_time, mod_time))
-
-        list_cache = os.path.join(back_cachedir, "base.p")
-        w_lock = os.path.join(back_cachedir, ".base.w")
-        ret = fileserver.check_file_list_cache(opts, "files", list_cache, w_lock)
-        assert (
-            ret[1] is True
-        ), "Cache file list cache file is not refreshed when future modification time"
-- 
2.43.0


openSUSE Build Service is sponsored by