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

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