Blame SOURCES/sos-collector-quote-all-options.patch

42837f
From c09e36927f74f955a325381a7d611ea270d04b65 Mon Sep 17 00:00:00 2001
42837f
From: Jake Hunsaker <jhunsake@redhat.com>
42837f
Date: Tue, 13 Nov 2018 12:30:45 -0500
42837f
Subject: [PATCH] [global] Quote options locally everywhere
42837f
42837f
An earlier commit passed cluster option values through pipes.quote() in
42837f
an effort to prevent command injection on the remote nodes, however this
42837f
did not capture every avenue through which a maliciously designed option
42837f
value or sos command could take.
42837f
42837f
Now, quote the option values at the time of use, and also quote any sos
42837f
option that isn't an on/off toggle.
42837f
42837f
Additionally, re-work how the ovirt/rhv database query is quoted and
42837f
filter out cluster/datacenter values that are likely to contain SQL
42837f
code, since we cannot rely on the driver to do this since we don't have
42837f
an actual connection to the database.
42837f
42837f
Original discovery of issues and patch work thanks to Riccardo Schirone.
42837f
42837f
Signed-off-by: Jake Hunsaker <jhunsake@redhat.com>
42837f
---
42837f
 soscollector/clusters/kubernetes.py |  3 ++-
42837f
 soscollector/clusters/ovirt.py      | 32 ++++++++++++++++++++++-------
42837f
 soscollector/configuration.py       |  2 +-
42837f
 soscollector/sos_collector.py       | 19 +++++++++--------
42837f
 soscollector/sosnode.py             | 13 ++++++------
42837f
 5 files changed, 46 insertions(+), 23 deletions(-)
42837f
42837f
diff --git a/soscollector/clusters/kubernetes.py b/soscollector/clusters/kubernetes.py
42837f
index 6c519f3..c5c2094 100644
42837f
--- a/soscollector/clusters/kubernetes.py
42837f
+++ b/soscollector/clusters/kubernetes.py
42837f
@@ -13,6 +13,7 @@
42837f
 # with this program; if not, write to the Free Software Foundation, Inc.,
42837f
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
42837f
 
42837f
+from pipes import quote
42837f
 from soscollector.clusters import Cluster
42837f
 
42837f
 
42837f
@@ -32,7 +33,7 @@ class kubernetes(Cluster):
42837f
     def get_nodes(self):
42837f
         self.cmd += ' get nodes'
42837f
         if self.get_option('label'):
42837f
-            self.cmd += ' -l %s ' % self.get_option('label')
42837f
+            self.cmd += ' -l %s ' % quote(self.get_option('label'))
42837f
         res = self.exec_master_cmd(self.cmd)
42837f
         if res['status'] == 0:
42837f
             nodes = []
42837f
diff --git a/soscollector/clusters/ovirt.py b/soscollector/clusters/ovirt.py
42837f
index 1c44c97..0a074ca 100644
42837f
--- a/soscollector/clusters/ovirt.py
42837f
+++ b/soscollector/clusters/ovirt.py
42837f
@@ -16,6 +16,7 @@
42837f
 import os
42837f
 import fnmatch
42837f
 
42837f
+from pipes import quote
42837f
 from soscollector.clusters import Cluster
42837f
 from getpass import getpass
42837f
 
42837f
@@ -31,6 +32,22 @@ class ovirt(Cluster):
42837f
         ('no-hypervisors', False, 'Do not collect from hypervisors')
42837f
     ]
42837f
 
42837f
+    def _sql_scrub(self, val):
42837f
+        '''
42837f
+        Manually sanitize SQL queries since we can't leave this up to the
42837f
+        driver since we do not have an actual DB connection
42837f
+        '''
42837f
+        if not val:
42837f
+            return '%'
42837f
+
42837f
+        invalid_chars = ['\x00', '\\', '\n', '\r', '\032', '"', '\'']
42837f
+        if any(x in invalid_chars for x in val):
42837f
+            self.log_warn("WARNING: Cluster option \'%s\' contains invalid "
42837f
+                          "characters. Using '%%' instead." % val)
42837f
+            return '%'
42837f
+
42837f
+        return val
42837f
+
42837f
     def setup(self):
42837f
         self.pg_pass = False
42837f
         if not self.get_option('no-database'):
42837f
@@ -38,13 +55,14 @@ class ovirt(Cluster):
42837f
         self.format_db_cmd()
42837f
 
42837f
     def format_db_cmd(self):
42837f
-        cluster = self.get_option('cluster') or '%'
42837f
-        datacenter = self.get_option('datacenter') or '%'
42837f
-        self.dbcmd = '/usr/share/ovirt-engine/dbscripts/engine-psql.sh -c \"'
42837f
-        self.dbcmd += ("select host_name from vds_static where cluster_id in "
42837f
-                       "(select cluster_id from cluster where name like \'%s\'"
42837f
-                       " and storage_pool_id in (select id from storage_pool "
42837f
-                       "where name like \'%s\'))\"" % (cluster, datacenter))
42837f
+        cluster = self._sql_scrub(self.get_option('cluster'))
42837f
+        datacenter = self._sql_scrub(self.get_option('datacenter'))
42837f
+        query = ("select host_name from vds_static where cluster_id in "
42837f
+                 "(select cluster_id from cluster where name like '%s'"
42837f
+                 " and storage_pool_id in (select id from storage_pool "
42837f
+                 "where name like '%s'))" % (cluster, datacenter))
42837f
+        self.dbcmd = ('/usr/share/ovirt-engine/dbscripts/engine-psql.sh '
42837f
+                      '-c {}'.format(quote(query)))
42837f
         self.log_debug('Query command for ovirt DB set to: %s' % self.dbcmd)
42837f
 
42837f
     def get_nodes(self):
42837f
diff --git a/soscollector/configuration.py b/soscollector/configuration.py
42837f
index ea33835..7f2c3c7 100644
42837f
--- a/soscollector/configuration.py
42837f
+++ b/soscollector/configuration.py
42837f
@@ -138,7 +138,7 @@ class Configuration(dict):
42837f
                 try:
42837f
                     # there are no instances currently where any cluster option
42837f
                     # should contain a legitimate space.
42837f
-                    value = pipes.quote(option.split('=')[1].split()[0])
42837f
+                    value = option.split('=')[1].split()[0]
42837f
                 except IndexError:
42837f
                     # conversion to boolean is handled during validation
42837f
                     value = 'True'
42837f
diff --git a/soscollector/sos_collector.py b/soscollector/sos_collector.py
42837f
index 70a3dc8..7331f63 100644
42837f
--- a/soscollector/sos_collector.py
42837f
+++ b/soscollector/sos_collector.py
42837f
@@ -32,6 +32,7 @@ from concurrent.futures import ThreadPoolExecutor
42837f
 from .sosnode import SosNode
42837f
 from distutils.sysconfig import get_python_lib
42837f
 from getpass import getpass
42837f
+from pipes import quote
42837f
 from six.moves import input
42837f
 from textwrap import fill
42837f
 from soscollector import __version__
42837f
@@ -360,32 +361,34 @@ this utility or remote systems that it connects to.
42837f
     def configure_sos_cmd(self):
42837f
         '''Configures the sosreport command that is run on the nodes'''
42837f
         if self.config['sos_opt_line']:
42837f
-            filt = ['&', '|', '>', '<']
42837f
+            filt = ['&', '|', '>', '<', ';']
42837f
             if any(f in self.config['sos_opt_line'] for f in filt):
42837f
                 self.log_warn('Possible shell script found in provided sos '
42837f
                               'command. Ignoring --sos-cmd option entirely.')
42837f
                 self.config['sos_opt_line'] = None
42837f
             else:
42837f
                 self.config['sos_cmd'] = '%s %s' % (
42837f
-                    self.config['sos_cmd'], self.config['sos_opt_line'])
42837f
+                    self.config['sos_cmd'], quote(self.config['sos_opt_line']))
42837f
                 self.log_debug("User specified manual sosreport command. "
42837f
                                "Command set to %s" % self.config['sos_cmd'])
42837f
                 return True
42837f
         if self.config['case_id']:
42837f
-            self.config['sos_cmd'] += ' --case-id=%s' % self.config['case_id']
42837f
+            self.config['sos_cmd'] += ' --case-id=%s' % (
42837f
+                quote(self.config['case_id']))
42837f
         if self.config['alloptions']:
42837f
             self.config['sos_cmd'] += ' --alloptions'
42837f
         if self.config['verify']:
42837f
             self.config['sos_cmd'] += ' --verify'
42837f
         if self.config['log_size']:
42837f
             self.config['sos_cmd'] += (' --log-size=%s'
42837f
-                                       % self.config['log_size'])
42837f
+                                       % quote(self.config['log_size']))
42837f
         if self.config['sysroot']:
42837f
-            self.config['sos_cmd'] += ' -s %s' % self.config['sysroot']
42837f
+            self.config['sos_cmd'] += ' -s %s' % quote(self.config['sysroot'])
42837f
         if self.config['chroot']:
42837f
-            self.config['sos_cmd'] += ' -c %s' % self.config['chroot']
42837f
+            self.config['sos_cmd'] += ' -c %s' % quote(self.config['chroot'])
42837f
         if self.config['compression']:
42837f
-            self.config['sos_cmd'] += ' -z %s' % self.config['compression']
42837f
+            self.config['sos_cmd'] += ' -z %s' % (
42837f
+                quote(self.config['compression']))
42837f
         self.log_debug('Initial sos cmd set to %s' % self.config['sos_cmd'])
42837f
 
42837f
     def connect_to_master(self):
42837f
@@ -408,7 +411,7 @@ this utility or remote systems that it connects to.
42837f
         can still be run if the user sets a --cluster-type manually
42837f
         '''
42837f
         checks = list(self.clusters.values())
42837f
-        for cluster in checks:
42837f
+        for cluster in self.clusters.values():
42837f
             checks.remove(cluster)
42837f
             cluster.master = self.master
42837f
             if cluster.check_enabled():
42837f
diff --git a/soscollector/sosnode.py b/soscollector/sosnode.py
42837f
index fde1e6a..3790433 100644
42837f
--- a/soscollector/sosnode.py
42837f
+++ b/soscollector/sosnode.py
42837f
@@ -26,6 +26,7 @@ import six
42837f
 import time
42837f
 
42837f
 from distutils.version import LooseVersion
42837f
+from pipes import quote
42837f
 from subprocess import Popen, PIPE
42837f
 
42837f
 
42837f
@@ -450,7 +451,7 @@ class SosNode():
42837f
 
42837f
         label = self.determine_sos_label()
42837f
         if label:
42837f
-            self.sos_cmd = ' %s %s' % (self.sos_cmd, label)
42837f
+            self.sos_cmd = ' %s %s' % (self.sos_cmd, quote(label))
42837f
 
42837f
         if self.config['sos_opt_line']:
42837f
             return True
42837f
@@ -464,7 +465,7 @@ class SosNode():
42837f
                                'enabled but do not exist' % not_only)
42837f
             only = self._fmt_sos_opt_list(self.config['only_plugins'])
42837f
             if only:
42837f
-                self.sos_cmd += ' --only-plugins=%s' % only
42837f
+                self.sos_cmd += ' --only-plugins=%s' % quote(only)
42837f
             return True
42837f
 
42837f
         if self.config['skip_plugins']:
42837f
@@ -477,7 +478,7 @@ class SosNode():
42837f
                                'already not enabled' % not_skip)
42837f
             skipln = self._fmt_sos_opt_list(skip)
42837f
             if skipln:
42837f
-                self.sos_cmd += ' --skip-plugins=%s' % skipln
42837f
+                self.sos_cmd += ' --skip-plugins=%s' % quote(skipln)
42837f
 
42837f
         if self.config['enable_plugins']:
42837f
             # only run enable for plugins that are disabled
42837f
@@ -490,18 +491,18 @@ class SosNode():
42837f
                                'are already enabled or do not exist' % not_on)
42837f
             enable = self._fmt_sos_opt_list(opts)
42837f
             if enable:
42837f
-                self.sos_cmd += ' --enable-plugins=%s' % enable
42837f
+                self.sos_cmd += ' --enable-plugins=%s' % quote(enable)
42837f
 
42837f
         if self.config['plugin_options']:
42837f
             opts = [o for o in self.config['plugin_options']
42837f
                     if self._plugin_exists(o.split('.')[0])
42837f
                     and self._plugin_option_exists(o.split('=')[0])]
42837f
             if opts:
42837f
-                self.sos_cmd += ' -k %s' % ','.join(o for o in opts)
42837f
+                self.sos_cmd += ' -k %s' % quote(','.join(o for o in opts))
42837f
 
42837f
         if self.config['preset']:
42837f
             if self._preset_exists(self.config['preset']):
42837f
-                self.sos_cmd += ' --preset=%s' % self.config['preset']
42837f
+                self.sos_cmd += ' --preset=%s' % quote(self.config['preset'])
42837f
             else:
42837f
                 self.log_debug('Requested to enable preset %s but preset does '
42837f
                                'not exist on node' % self.config['preset'])
42837f
-- 
42837f
2.17.2
42837f