Skip to content

Instantly share code, notes, and snippets.

@mirceaulinic
Created May 7, 2020 08:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mirceaulinic/e4815246dde93c5da27e6cee5f756d49 to your computer and use it in GitHub Desktop.
Save mirceaulinic/e4815246dde93c5da27e6cee5f756d49 to your computer and use it in GitHub Desktop.
CVE-2020-11651 and CVE-2020-11652 patches for Salt 2018.3.x
From ac9691f3c86bb7fd27ae84bff620addfbec8853d Mon Sep 17 00:00:00 2001
From: "Daniel A. Wozniak" <dwozniak@saltstack.com>
Date: Fri, 24 Apr 2020 18:01:01 +0000
Subject: [PATCH] CVE-2020-11651 and CVE-2020-11652
---
salt/master.py | 58 +++++++++++++++++++++++++++++++++-------
salt/tokens/localfs.py | 3 +++
salt/utils/verify.py | 57 +++++++++++++++++++++++++++++++++++----
salt/wheel/config.py | 8 +++++-
salt/wheel/file_roots.py | 7 ++++-
5 files changed, 117 insertions(+), 16 deletions(-)
diff --git a/salt/master.py b/salt/master.py
index 30983c3068..ebc3a2c7fc 100644
--- a/salt/master.py
+++ b/salt/master.py
@@ -1050,12 +1050,13 @@ class MWorker(salt.utils.process.SignalHandlingMultiprocessingProcess):
'''
log.trace('Clear payload received with command %s', load['cmd'])
cmd = load['cmd']
- if cmd.startswith('__'):
- return False
+ method = self.clear_funcs.get_method(cmd)
+ if not method:
+ return {}, {'fun': 'send_clear'}
if self.opts['master_stats']:
start = time.time()
self.stats[cmd]['runs'] += 1
- ret = getattr(self.clear_funcs, cmd)(load), {'fun': 'send_clear'}
+ ret = method(load), {'fun': 'send_clear'}
if self.opts['master_stats']:
self._post_stats(start, cmd)
return ret
@@ -1073,8 +1074,9 @@ class MWorker(salt.utils.process.SignalHandlingMultiprocessingProcess):
return {}
cmd = data['cmd']
log.trace('AES payload received with command %s', data['cmd'])
- if cmd.startswith('__'):
- return False
+ method = self.aes_funcs.get_method(cmd)
+ if not method:
+ return {}, {'fun': 'send'}
if self.opts['master_stats']:
start = time.time()
self.stats[cmd]['runs'] += 1
@@ -1097,13 +1099,44 @@ class MWorker(salt.utils.process.SignalHandlingMultiprocessingProcess):
self.__bind()
+class TransportMethods(object):
+ '''
+ Expose methods to the transport layer, methods with their names found in
+ the class attribute 'expose_methods' will be exposed to the transport layer
+ via 'get_method'.
+ '''
+
+ expose_methods = ()
+
+ def get_method(self, name):
+ '''
+ Get a method which should be exposed to the transport layer
+ '''
+ if name in self.expose_methods:
+ try:
+ return getattr(self, name)
+ except AttributeError:
+ log.error("Expose method not found: %s", name)
+ else:
+ log.error("Requested method not exposed: %s", name)
+
+
# TODO: rename? No longer tied to "AES", just "encrypted" or "private" requests
-class AESFuncs(object):
+class AESFuncs(TransportMethods):
'''
Set up functions that are available when the load is encrypted with AES
'''
- # The AES Functions:
- #
+
+ expose_methods = (
+ 'verify_minion', '_master_tops', '_ext_nodes', '_master_opts',
+ '_mine_get', '_mine', '_mine_delete', '_mine_flush', '_file_recv',
+ '_pillar', '_minion_event', '_handle_minion_event', '_return',
+ '_syndic_return', 'minion_runner', 'pub_ret', 'minion_pub',
+ 'minion_publish', 'revoke_auth', 'run_func', '_serve_file',
+ '_file_find', '_file_hash', '_file_find_and_stat', '_file_list',
+ '_file_list_emptydirs', '_dir_list', '_symlink_list', '_file_envs',
+ )
+
def __init__(self, opts):
'''
Create a new AESFuncs
@@ -1817,11 +1850,18 @@ class AESFuncs(object):
return ret, {'fun': 'send'}
-class ClearFuncs(object):
+class ClearFuncs(TransportMethods):
'''
Set up functions that are safe to execute when commands sent to the master
without encryption and authentication
'''
+
+ # These methods will be exposed to the transport layer by
+ # MWorker._handle_clear
+ expose_methods = (
+ 'ping', 'publish', 'get_token', 'mk_token', 'wheel', 'runner',
+ )
+
# The ClearFuncs object encapsulates the functions that can be executed in
# the clear:
# publish (The publish from the LocalClient)
diff --git a/salt/tokens/localfs.py b/salt/tokens/localfs.py
index 021bdb9e50..590a096306 100644
--- a/salt/tokens/localfs.py
+++ b/salt/tokens/localfs.py
@@ -12,6 +12,7 @@ import logging
import salt.utils.files
import salt.utils.path
+import salt.utils.verify
import salt.payload
from salt.ext import six
@@ -59,6 +60,8 @@ def get_token(opts, tok):
:returns: Token data if successful. Empty dict if failed.
'''
t_path = os.path.join(opts['token_dir'], tok)
+ if not salt.utils.verify.clean_path(opts['token_dir'], t_path):
+ return {}
if not os.path.isfile(t_path):
return {}
serial = salt.payload.Serial(opts)
diff --git a/salt/utils/verify.py b/salt/utils/verify.py
index 5eb8481069..f289b65b4c 100644
--- a/salt/utils/verify.py
+++ b/salt/utils/verify.py
@@ -31,6 +31,7 @@ import salt.utils.files
import salt.utils.path
import salt.utils.platform
import salt.utils.user
+import salt.ext.six
log = logging.getLogger(__name__)
@@ -472,23 +473,69 @@ def check_max_open_files(opts):
log.log(level=level, msg=msg)
+def _realpath_darwin(path):
+ base = ''
+ for part in path.split(os.path.sep)[1:]:
+ if base != '':
+ if os.path.islink(os.path.sep.join([base, part])):
+ base = os.readlink(os.path.sep.join([base, part]))
+ else:
+ base = os.path.abspath(os.path.sep.join([base, part]))
+ else:
+ base = os.path.abspath(os.path.sep.join([base, part]))
+ return base
+
+
+def _realpath_windows(path):
+ base = ''
+ for part in path.split(os.path.sep):
+ if base != '':
+ try:
+ part = os.readlink(os.path.sep.join([base, part]))
+ base = os.path.abspath(part)
+ except OSError:
+ base = os.path.abspath(os.path.sep.join([base, part]))
+ else:
+ base = part
+ return base
+
+
+def _realpath(path):
+ '''
+ Cross platform realpath method. On Windows when python 3, this method
+ uses the os.readlink method to resolve any filesystem links. On Windows
+ when python 2, this method is a no-op. All other platforms and version use
+ os.realpath
+ '''
+ if salt.utils.platform.is_darwin():
+ return _realpath_darwin(path)
+ elif salt.utils.platform.is_windows():
+ if salt.ext.six.PY3:
+ return _realpath_windows(path)
+ else:
+ return path
+ return os.path.realpath(path)
+
+
def clean_path(root, path, subdir=False):
'''
Accepts the root the path needs to be under and verifies that the path is
under said root. Pass in subdir=True if the path can result in a
subdirectory of the root instead of having to reside directly in the root
'''
- if not os.path.isabs(root):
+ real_root = _realpath(root)
+ if not os.path.isabs(real_root):
return ''
if not os.path.isabs(path):
path = os.path.join(root, path)
path = os.path.normpath(path)
+ real_path = _realpath(path)
if subdir:
- if path.startswith(root):
- return path
+ if real_path.startswith(real_root):
+ return real_path
else:
- if os.path.dirname(path) == os.path.normpath(root):
- return path
+ if os.path.dirname(real_path) == os.path.normpath(real_root):
+ return real_path
return ''
diff --git a/salt/wheel/config.py b/salt/wheel/config.py
index a8a93c53e5..3984444f8f 100644
--- a/salt/wheel/config.py
+++ b/salt/wheel/config.py
@@ -75,13 +75,19 @@ def update_config(file_name, yaml_contents):
dir_path = os.path.join(__opts__['config_dir'],
os.path.dirname(__opts__['default_include']))
try:
- yaml_out = salt.utils.yaml.safe_dump(yaml_contents, default_flow_style=False)
+ yaml_out = salt.utils.yaml.safe_dump(
+ yaml_contents,
+ default_flow_style=False,
+ )
if not os.path.exists(dir_path):
log.debug('Creating directory %s', dir_path)
os.makedirs(dir_path, 0o755)
file_path = os.path.join(dir_path, file_name)
+ if not salt.utils.verify.clean_path(dir_path, file_path):
+ return 'Invalid path'
+
with salt.utils.files.fopen(file_path, 'w') as fp_:
fp_.write(yaml_out)
diff --git a/salt/wheel/file_roots.py b/salt/wheel/file_roots.py
index 02cc8c5b32..ad42335734 100644
--- a/salt/wheel/file_roots.py
+++ b/salt/wheel/file_roots.py
@@ -25,6 +25,8 @@ def find(path, saltenv='base'):
return ret
for root in __opts__['file_roots'][saltenv]:
full = os.path.join(root, path)
+ if not salt.utils.verify.clean_path(root, full):
+ continue
if os.path.isfile(full):
# Add it to the dict
with salt.utils.files.fopen(full, 'rb') as fp_:
@@ -107,7 +109,10 @@ def write(data, path, saltenv='base', index=0):
if os.path.isabs(path):
return ('The path passed in {0} is not relative to the environment '
'{1}').format(path, saltenv)
- dest = os.path.join(__opts__['file_roots'][saltenv][index], path)
+ root = __opts__['file_roots'][saltenv][index]
+ dest = os.path.join(root, path)
+ if not salt.utils.verify.clean_path(root, dest, subdir=True):
+ return 'Invalid path: {}'.format(path)
dest_dir = os.path.dirname(dest)
if not os.path.isdir(dest_dir):
os.makedirs(dest_dir)
--
2.20.1
@mirceaulinic
Copy link
Author

Apply the patch:

# patch -u -b -p 1 -d /usr/lib/python2.7/site-packages <  CVE-2020-1165_2018.3.x.patch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment