File backintime-security_hardening_backport.patch of Package backintime

--- common/backintime.orig
+++ common/backintime
@@ -24,4 +24,4 @@ else
 	APP_PATH=$(readlink -m "${CUR_PATH}/../share/backintime/common")
 fi
 
-python3 $APP_PATH/backintime.py "$@"
+python3 -Es $APP_PATH/backintime.py "$@"
--- common/backintime-askpass.orig
+++ common/backintime-askpass
@@ -28,4 +28,4 @@ else
 	APP_PATH=$(readlink -m "${CUR_PATH}/../share/backintime/common")
 fi
 
-python3 $APP_PATH/askpass.py "$@"
+python3 -Es $APP_PATH/askpass.py "$@"
--- common/config.py.orig
+++ common/config.py
@@ -36,7 +36,7 @@ import sshtools
 import encfstools
 import password
 import pluginmanager
-from exceptions import PermissionDeniedByPolicy, InvalidChar
+from exceptions import PermissionDeniedByPolicy, InvalidChar, InvalidCmd, LimitExceeded
 
 _=gettext.gettext
 
@@ -930,7 +930,7 @@ class Config( configfile.ConfigFileWithP
         self.set_profile_bool_value( 'snapshots.backup_on_restore.enabled', value, profile_id )
 
     def is_run_nice_from_cron_enabled( self, profile_id = None ):
-        #?Run cronjobs with 'nice \-n 19'. This will give BackInTime the
+        #?Run cronjobs with 'nice \-n19'. This will give BackInTime the
         #?lowest CPU priority to not interupt any other working process.
         return self.get_profile_bool_value( 'snapshots.cron.nice', self.DEFAULT_RUN_NICE_FROM_CRON, profile_id )
 
@@ -955,7 +955,7 @@ class Config( configfile.ConfigFileWithP
         self.set_profile_bool_value( 'snapshots.user_backup.ionice', value, profile_id )
 
     def is_run_nice_on_remote_enabled(self, profile_id = None):
-        #?Run rsync and other commands on remote host with 'nice \-n 19'
+        #?Run rsync and other commands on remote host with 'nice \-n19'
         return self.get_profile_bool_value('snapshots.ssh.nice', self.DEFAULT_RUN_NICE_ON_REMOTE, profile_id)
 
     def set_run_nice_on_remote_enabled(self, value, profile_id = None):
@@ -1530,7 +1530,7 @@ class Config( configfile.ConfigFileWithP
                 self.set_profile_str_value('snapshots.path.uuid', uuid, profile_id)
             try:
                 self.setupUdev.addRule(self.cron_cmd(profile_id), uuid)
-            except InvalidChar as e:
+            except (InvalidChar, InvalidCmd, LimitExceeded) as e:
                 logger.error(str(e), self)
                 self.notify_error(str(e))
                 return False
@@ -1560,7 +1560,7 @@ class Config( configfile.ConfigFileWithP
         if self.is_run_ionice_from_cron_enabled(profile_id) and tools.check_command('ionice'):
             cmd = tools.which('ionice') + ' -c2 -n7 ' + cmd
         if self.is_run_nice_from_cron_enabled( profile_id ) and tools.check_command('nice'):
-            cmd = tools.which('nice') + ' -n 19 ' + cmd
+            cmd = tools.which('nice') + ' -n19 ' + cmd
         return cmd
 
 if __name__ == "__main__":
--- common/exceptions.py.orig
+++ common/exceptions.py
@@ -39,6 +39,20 @@ class InvalidChar(BackInTimeException):
     def __str__(self):
         return self.msg
 
+class InvalidCmd(BackInTimeException):
+    def __init__(self, msg):
+        self.msg = msg
+
+    def __str__(self):
+        return self.msg
+
+class LimitExceeded(BackInTimeException):
+    def __init__(self, msg):
+        self.msg = msg
+
+    def __str__(self):
+        return self.msg
+
 class PermissionDeniedByPolicy(BackInTimeException):
     def __init__(self, msg):
         self.msg = msg
--- common/password.py.orig
+++ common/password.py
@@ -52,7 +52,7 @@ class Password_Cache(tools.Daemon):
             os.mkdir(pw_cache_path, 0o700)
         else:
             os.chmod(pw_cache_path, 0o700)
-        super(Password_Cache, self).__init__(self.config.get_password_cache_pid(), *args, **kwargs)
+        super(Password_Cache, self).__init__(self.config.get_password_cache_pid(), umask=0o077, *args, **kwargs)
         self.db_keyring = {}
         self.db_usr = {}
         self.fifo = password_ipc.FIFO(self.config.get_password_cache_fifo())
--- common/tools.py.orig
+++ common/tools.py
@@ -53,7 +53,7 @@ except ImportError:
 import configfile
 import logger
 from applicationinstance import ApplicationInstance
-from exceptions import Timeout, InvalidChar, PermissionDeniedByPolicy
+from exceptions import Timeout, InvalidChar, InvalidCmd, LimitExceeded, PermissionDeniedByPolicy
 
 ON_AC = 0
 ON_BATTERY = 1
@@ -379,7 +379,7 @@ def _execute( cmd, callback = None, user
 
 def is_process_alive( pid ):
     try:
-        os.kill( pid, 0 )	#this will raise an exception if the pid is not valid
+        os.kill(pid, 0) #this will raise an exception if the pid is not valid
     except:
         return False
 
@@ -1231,6 +1231,10 @@ class SetupUdev(object):
         except dbus.exceptions.DBusException as e:
             if e._dbus_error_name == 'net.launchpad.backintime.InvalidChar':
                 raise InvalidChar(str(e))
+            elif e._dbus_error_name == 'net.launchpad.backintime.InvalidCmd':
+                raise InvalidCmd(str(e))
+            elif e._dbus_error_name == 'net.launchpad.backintime.LimitExceeded':
+                raise LimitExceeded(str(e))
             else:
                 raise
 
@@ -1299,11 +1303,12 @@ class Daemon:
     License CC BY-SA 3.0
     http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
     """
-    def __init__(self, pidfile = None, stdin='/dev/null', stdout='/dev/stdout', stderr='/dev/null'):
+    def __init__(self, pidfile = None, stdin='/dev/null', stdout='/dev/stdout', stderr='/dev/null', umask=0o022):
         self.stdin = stdin
         self.stdout = stdout
         self.stderr = stderr
         self.pidfile = pidfile
+        self.umask = umask
         if pidfile:
             self.appInstance = ApplicationInstance(pidfile, auto_exit = False, flock = False)
 
@@ -1327,7 +1332,7 @@ class Daemon:
         logger.debug('decouple from parent environment', self)
         os.chdir("/")
         os.setsid()
-        os.umask(0)
+        os.umask(self.umask)
 
         # do second fork
         try:
--- qt4/backintime-qt4.orig
+++ qt4/backintime-qt4
@@ -28,4 +28,4 @@ else
 	APP_PATH=$(readlink -m "${CUR_PATH}/../share/backintime/qt4")
 fi
 
-python3 ${APP_PATH}/app.py "$@"
+python3 -Es ${APP_PATH}/app.py "$@"
--- qt4/net.launchpad.backintime.serviceHelper.conf.orig
+++ qt4/net.launchpad.backintime.serviceHelper.conf
@@ -7,13 +7,11 @@
   <!-- Only root can own the service -->
   <policy user="root">
     <allow own="net.launchpad.backintime.serviceHelper"/>
-    <allow send_destination="net.launchpad.backintime.serviceHelper"/>
-    <allow send_interface="net.launchpad.backintime.serviceHelper.UdevRules"/>
+    <allow send_destination="net.launchpad.backintime.serviceHelper" send_interface="net.launchpad.backintime.serviceHelper.UdevRules"/>
   </policy>
 
   <!-- Allow anyone to invoke methods on the interfaces -->
   <policy context="default">
-    <deny own="net.launchpad.backintime.serviceHelper"/>
-    <allow send_destination="net.launchpad.backintime.serviceHelper"/>
+    <allow send_destination="net.launchpad.backintime.serviceHelper" send_interface="net.launchpad.backintime.serviceHelper.UdevRules"/>
   </policy>
 </busconfig>
--- qt4/net.launchpad.backintime.serviceHelper.service.orig
+++ qt4/net.launchpad.backintime.serviceHelper.service
@@ -1,4 +1,4 @@
 [D-BUS Service]
 Name=net.launchpad.backintime.serviceHelper
-Exec=/usr/bin/python3 /usr/share/backintime/qt4/serviceHelper.py
+Exec=/usr/bin/python3 -Es /usr/share/backintime/qt4/serviceHelper.py
 User=root
--- qt4/serviceHelper.py.orig
+++ qt4/serviceHelper.py
@@ -79,6 +79,12 @@ UDEV_RULES_PATH = '/etc/udev/rules.d/99-
 class InvalidChar(dbus.DBusException):
     _dbus_error_name = 'net.launchpad.backintime.InvalidChar'
 
+class InvalidCmd(dbus.DBusException):
+    _dbus_error_name = 'net.launchpad.backintime.InvalidCmd'
+
+class LimitExceeded(dbus.DBusException):
+    _dbus_error_name = 'net.launchpad.backintime.LimitExceeded'
+
 class PermissionDeniedByPolicy(dbus.DBusException):
     _dbus_error_name = 'com.ubuntu.DeviceDriver.PermissionDeniedByPolicy'
 
@@ -93,10 +99,61 @@ class UdevRules(dbus.service.Object):
         self.tmpDict = {}
 
         #find su path
-        proc = Popen(['which', 'su'], stdout = PIPE)
-        self.su = proc.communicate()[0].strip().decode()
-        if proc.returncode or not self.su:
-            self.su = '/bin/su'
+        self.su = self._which('su', '/bin/su')
+        self.backintime = self._which('backintime', '/usr/bin/backintime')
+        self.nice = self._which('nice', '/usr/bin/nice')
+        self.ionice = self._which('ionice', '/usr/bin/ionice')
+        self.max_rules = 100
+        self.max_users = 20
+        self.max_cmd_len = 100
+
+    def _which(self, exe, fallback):
+        proc = Popen(['which', exe], stdout = PIPE)
+        ret = proc.communicate()[0].strip().decode()
+        if proc.returncode or not ret:
+            return fallback
+
+        return ret
+
+    def _validateCmd(self, cmd):
+
+        if cmd.find("&&") != -1:
+            raise InvalidCmd("Parameter 'cmd' contains '&&' concatenation")
+        # make sure it starts with an absolute path
+        elif not cmd.startswith(os.path.sep):
+            raise InvalidCmd("Parameter 'cmd' does not start with '/'")
+
+        parts = cmd.split()
+
+        # make sure only well known commands and switches are used
+        whitelist = (
+            (self.nice, ("-n")),
+            (self.ionice, ("-c", "-n")),
+        )
+
+        for c, switches in whitelist:
+            if parts and parts[0] == c:
+                parts.pop(0)
+                for sw in switches:
+                    while parts and parts[0].startswith(sw):
+                        parts.pop(0)
+
+        if not parts:
+            raise InvalidCmd("Parameter 'cmd' does not contain the backintime command")
+        elif parts[0] != self.backintime:
+            raise InvalidCmd("Parameter 'cmd' contains non-whitelisted cmd/parameter (%s)" % parts[0])
+
+    def _checkLimits(self, owner, cmd):
+
+        if len(self.tmpDict.get(owner, [])) >= self.max_rules:
+            raise LimitExceeded("Maximum number of cached rules reached (%d)"
+                            % self.max_rules)
+        elif len(self.tmpDict) >= self.max_users:
+            raise LimitExceeded("Maximum number of cached users reached (%d)"
+                            % self.max_users)
+        elif len(cmd) > self.max_cmd_len:
+            raise LimitExceeded("Maximum length of command line reached (%d)"
+                            % self.max_cmd_len)
 
     @dbus.service.method("net.launchpad.backintime.serviceHelper.UdevRules",
                          in_signature='ss', out_signature='',
@@ -117,10 +174,14 @@ class UdevRules(dbus.service.Object):
             raise InvalidChar("Parameter 'uuid' contains invalid character(s) %s"
                               % '|'.join(set(chars)) )
 
+        self._validateCmd(cmd)
+
         info = SenderInfo(sender, conn)
         user = info.connectionUnixUser()
         owner = info.nameOwner()
 
+        self._checkLimits(owner, cmd)
+
         #create su command
         sucmd = "%s - '%s' -c '%s'" %(self.su, user, cmd)
         #create Udev rule
openSUSE Build Service is sponsored by