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