0b8af757b6
SELinux pythons applications should not allow the user to change the sys.path Signed-off-by: Eric Paris <eparis@redhat.com> Acked-by: Dan Walsh <dwalsh@redhat.com>
417 lines
15 KiB
Python
417 lines
15 KiB
Python
#! /usr/bin/python -Es
|
|
# Authors: Dan Walsh <dwalsh@redhat.com>
|
|
# Authors: Josh Cogliati
|
|
#
|
|
# Copyright (C) 2009,2010 Red Hat
|
|
# see file 'COPYING' for use and warranty information
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License as
|
|
# published by the Free Software Foundation; version 2 only
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
|
|
import os, sys, socket, random, fcntl, shutil, re, subprocess
|
|
import selinux
|
|
import signal
|
|
from tempfile import mkdtemp
|
|
import pwd
|
|
|
|
PROGNAME = "policycoreutils"
|
|
HOMEDIR=pwd.getpwuid(os.getuid()).pw_dir
|
|
|
|
import gettext
|
|
gettext.bindtextdomain(PROGNAME, "/usr/share/locale")
|
|
gettext.textdomain(PROGNAME)
|
|
|
|
try:
|
|
gettext.install(PROGNAME,
|
|
localedir = "/usr/share/locale",
|
|
unicode=False,
|
|
codeset = 'utf-8')
|
|
except IOError:
|
|
import __builtin__
|
|
__builtin__.__dict__['_'] = unicode
|
|
|
|
DEFAULT_TYPE = "sandbox_t"
|
|
DEFAULT_X_TYPE = "sandbox_x_t"
|
|
SAVE_FILES = {}
|
|
|
|
random.seed(None)
|
|
|
|
def sighandler(signum, frame):
|
|
signal.signal(signum, signal.SIG_IGN)
|
|
os.kill(0, signum)
|
|
raise KeyboardInterrupt
|
|
|
|
def setup_sighandlers():
|
|
signal.signal(signal.SIGHUP, sighandler)
|
|
signal.signal(signal.SIGQUIT, sighandler)
|
|
signal.signal(signal.SIGTERM, sighandler)
|
|
|
|
def error_exit(msg):
|
|
sys.stderr.write("%s: " % sys.argv[0])
|
|
sys.stderr.write("%s\n" % msg)
|
|
sys.stderr.flush()
|
|
sys.exit(1)
|
|
|
|
def copyfile(file, dir, dest):
|
|
import re
|
|
if file.startswith(dir):
|
|
dname = os.path.dirname(file)
|
|
bname = os.path.basename(file)
|
|
if dname == dir:
|
|
dest = dest + "/" + bname
|
|
else:
|
|
newdir = re.sub(dir, dest, dname)
|
|
if not os.path.exists(newdir):
|
|
os.makedirs(newdir)
|
|
dest = newdir + "/" + bname
|
|
|
|
try:
|
|
if os.path.isdir(file):
|
|
shutil.copytree(file, dest)
|
|
else:
|
|
shutil.copy2(file, dest)
|
|
except shutil.Error, elist:
|
|
for e in elist:
|
|
sys.stderr.write(e[1])
|
|
|
|
SAVE_FILES[file] = (dest, os.path.getmtime(dest))
|
|
|
|
def savefile(new, orig, X_ind):
|
|
copy = False
|
|
if(X_ind):
|
|
import gtk
|
|
dlg = gtk.MessageDialog(None, 0, gtk.MESSAGE_INFO,
|
|
gtk.BUTTONS_YES_NO,
|
|
_("Do you want to save changes to '%s' (Y/N): ") % orig)
|
|
dlg.set_title(_("Sandbox Message"))
|
|
dlg.set_position(gtk.WIN_POS_MOUSE)
|
|
dlg.show_all()
|
|
rc = dlg.run()
|
|
dlg.destroy()
|
|
if rc == gtk.RESPONSE_YES:
|
|
copy = True
|
|
else:
|
|
ans = raw_input(_("Do you want to save changes to '%s' (y/N): ") % orig)
|
|
if(re.match(_("[yY]"),ans)):
|
|
copy = True
|
|
if(copy):
|
|
shutil.copy2(new,orig)
|
|
|
|
def reserve(level):
|
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
sock.bind("\0%s" % level)
|
|
fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, fcntl.FD_CLOEXEC)
|
|
|
|
def gen_mcs():
|
|
while True:
|
|
i1 = random.randrange(0, 1024)
|
|
i2 = random.randrange(0, 1024)
|
|
if i1 == i2:
|
|
continue
|
|
if i1 > i2:
|
|
tmp = i1
|
|
i1 = i2
|
|
i2 = tmp
|
|
level = "s0:c%d,c%d" % (i1, i2)
|
|
try:
|
|
reserve(level)
|
|
except socket.error:
|
|
continue
|
|
break
|
|
return level
|
|
|
|
def fullpath(cmd):
|
|
for i in [ "/", "./", "../" ]:
|
|
if cmd.startswith(i):
|
|
return cmd
|
|
for i in os.environ["PATH"].split(':'):
|
|
f = "%s/%s" % (i, cmd)
|
|
if os.access(f, os.X_OK):
|
|
return f
|
|
return cmd
|
|
|
|
class Sandbox:
|
|
VERSION = "sandbox .1"
|
|
SYSLOG = "/var/log/messages"
|
|
|
|
def __init__(self):
|
|
self.setype = DEFAULT_TYPE
|
|
self.__options = None
|
|
self.__cmds = None
|
|
self.__init_files = []
|
|
self.__paths = []
|
|
self.__mount = False
|
|
self.__level = None
|
|
self.__homedir = None
|
|
self.__tmpdir = None
|
|
|
|
def __validate_mount(self):
|
|
if self.__options.level:
|
|
if not self.__options.homedir or not self.__options.tmpdir:
|
|
self.usage(_("Homedir and tempdir required for level mounts"))
|
|
|
|
if not os.path.exists("/usr/sbin/seunshare"):
|
|
raise ValueError(_("""
|
|
/usr/sbin/seunshare is required for the action you want to perform.
|
|
"""))
|
|
|
|
def __mount_callback(self, option, opt, value, parser):
|
|
self.__mount = True
|
|
|
|
def __x_callback(self, option, opt, value, parser):
|
|
self.__mount = True
|
|
setattr(parser.values, option.dest, True)
|
|
|
|
def __validdir(self, option, opt, value, parser):
|
|
if not os.path.isdir(value):
|
|
raise IOError("Directory "+value+" not found")
|
|
setattr(parser.values, option.dest, value)
|
|
self.__mount = True
|
|
|
|
def __include(self, option, opt, value, parser):
|
|
rp = os.path.realpath(os.path.expanduser(value))
|
|
if not os.path.exists(rp):
|
|
raise IOError(value+" not found")
|
|
|
|
if rp not in self.__init_files:
|
|
self.__init_files.append(rp)
|
|
|
|
def __includefile(self, option, opt, value, parser):
|
|
fd = open(value, "r")
|
|
for i in fd.readlines():
|
|
try:
|
|
self.__include(option, opt, i[:-1], parser)
|
|
except IOError, e:
|
|
sys.stderr.write(str(e))
|
|
fd.close()
|
|
|
|
def __copyfiles(self):
|
|
files = self.__init_files + self.__paths
|
|
homedir=pwd.getpwuid(os.getuid()).pw_dir
|
|
for f in files:
|
|
copyfile(f, homedir, self.__homedir)
|
|
copyfile(f, "/tmp", self.__tmpdir)
|
|
|
|
def __setup_sandboxrc(self, wm = "/usr/bin/matchbox-window-manager -use_titlebar no"):
|
|
execfile =self.__homedir + "/.sandboxrc"
|
|
fd = open(execfile, "w+")
|
|
if self.__options.session:
|
|
fd.write("""#!/bin/sh
|
|
#TITLE: /etc/gdm/Xsession
|
|
/etc/gdm/Xsession
|
|
""")
|
|
else:
|
|
command = " ".join(self.__paths)
|
|
fd.write("""#! /bin/sh
|
|
#TITLE: %s
|
|
/usr/bin/test -r ~/.xmodmap && /usr/bin/xmodmap ~/.xmodmap
|
|
%s &
|
|
WM_PID=$!
|
|
%s
|
|
kill -TERM $WM_PID 2> /dev/null
|
|
""" % (command, wm, command))
|
|
fd.close()
|
|
os.chmod(execfile, 0700)
|
|
|
|
def usage(self, message = ""):
|
|
error_exit("%s\n%s" % (self.__parser.usage, message))
|
|
|
|
def __parse_options(self):
|
|
from optparse import OptionParser
|
|
usage = _("""
|
|
sandbox [-h] [-[X|M] [-l level ] [-H homedir] [-T tempdir]] [-I includefile ] [-W windowmanager ] [[-i file ] ...] [ -t type ] command
|
|
|
|
sandbox [-h] [-[X|M] [-l level ] [-H homedir] [-T tempdir]] [-I includefile ] [-W windowmanager ] [[-i file ] ...] [ -t type ] -S
|
|
""")
|
|
|
|
parser = OptionParser(version=self.VERSION, usage=usage)
|
|
parser.disable_interspersed_args()
|
|
parser.add_option("-i", "--include",
|
|
action="callback", callback=self.__include,
|
|
type="string",
|
|
help=_("include file in sandbox"))
|
|
parser.add_option("-I", "--includefile", action="callback", callback=self.__includefile,
|
|
type="string",
|
|
help=_("read list of files to include in sandbox from INCLUDEFILE"))
|
|
parser.add_option("-t", "--type", dest="setype", action="store", default=None,
|
|
help=_("run sandbox with SELinux type"))
|
|
parser.add_option("-M", "--mount",
|
|
action="callback", callback=self.__mount_callback,
|
|
help=_("mount new home and/or tmp directory"))
|
|
|
|
parser.add_option("-S", "--session", action="store_true", dest="session",
|
|
default=False, help=_("run complete desktop session within sandbox"))
|
|
|
|
parser.add_option("-X", dest="X_ind",
|
|
action="callback", callback=self.__x_callback,
|
|
default=False, help=_("run X application within a sandbox"))
|
|
|
|
parser.add_option("-H", "--homedir",
|
|
action="callback", callback=self.__validdir,
|
|
type="string",
|
|
dest="homedir",
|
|
help=_("alternate home directory to use for mounting"))
|
|
|
|
parser.add_option("-T", "--tmpdir", dest="tmpdir",
|
|
type="string",
|
|
action="callback", callback=self.__validdir,
|
|
help=_("alternate /tmp directory to use for mounting"))
|
|
|
|
parser.add_option("-W", "--windowmanager", dest="wm",
|
|
type="string",
|
|
default="/usr/bin/matchbox-window-manager -use_titlebar no",
|
|
help=_("alternate window manager"))
|
|
|
|
parser.add_option("-l", "--level", dest="level",
|
|
help=_("MCS/MLS level for the sandbox"))
|
|
|
|
self.__parser=parser
|
|
|
|
self.__options, cmds = parser.parse_args()
|
|
|
|
if self.__options.X_ind:
|
|
self.setype = DEFAULT_X_TYPE
|
|
|
|
if self.__options.setype:
|
|
self.setype = self.__options.setype
|
|
|
|
if self.__mount:
|
|
self.__validate_mount()
|
|
|
|
if self.__options.session:
|
|
if not self.__options.setype:
|
|
self.setype = selinux.getcon()[1].split(":")[2]
|
|
if not self.__options.homedir or not self.__options.tmpdir:
|
|
self.usage(_("You must specify a Homedir and tempdir when setting up a session sandbox"))
|
|
if len(cmds) > 0:
|
|
self.usage(_("Commands are not allowed in a session sandbox"))
|
|
self.__options.X_ind = True
|
|
self.__homedir = self.__options.homedir
|
|
self.__tmpdir = self.__options.tmpdir
|
|
else:
|
|
if len(cmds) == 0:
|
|
self.usage(_("Command required"))
|
|
cmds[0] = fullpath(cmds[0])
|
|
if not os.access(cmds[0], os.X_OK):
|
|
self.usage(_("%s is not an executable") % cmds[0] )
|
|
|
|
self.__cmds = cmds
|
|
|
|
for f in cmds:
|
|
rp = os.path.realpath(f)
|
|
if os.path.exists(rp):
|
|
self.__paths.append(rp)
|
|
else:
|
|
self.__paths.append(f)
|
|
|
|
def __gen_context(self):
|
|
if self.__options.level:
|
|
level = self.__options.level
|
|
else:
|
|
level = gen_mcs()
|
|
|
|
con = selinux.getcon()[1].split(":")
|
|
self.__execcon = "%s:%s:%s:%s" % (con[0], con[1], self.setype, level)
|
|
self.__filecon = "%s:%s:%s:%s" % (con[0], "object_r",
|
|
"%s_file_t" % self.setype[:-2],
|
|
level)
|
|
def __setup_dir(self):
|
|
if self.__options.level or self.__options.session:
|
|
return
|
|
sandboxdir = HOMEDIR + "/.sandbox"
|
|
if not os.path.exists(sandboxdir):
|
|
os.mkdir(sandboxdir)
|
|
|
|
if self.__options.homedir:
|
|
selinux.chcon(self.__options.homedir, self.__filecon, recursive=True)
|
|
self.__homedir = self.__options.homedir
|
|
else:
|
|
selinux.setfscreatecon(self.__filecon)
|
|
self.__homedir = mkdtemp(dir=sandboxdir, prefix=".sandbox")
|
|
|
|
if self.__options.tmpdir:
|
|
selinux.chcon(self.__options.tmpdir, self.__filecon, recursive=True)
|
|
self.__tmpdir = self.__options.tmpdir
|
|
else:
|
|
selinux.setfscreatecon(self.__filecon)
|
|
self.__tmpdir = mkdtemp(dir="/tmp", prefix=".sandbox")
|
|
selinux.setfscreatecon(None)
|
|
self.__copyfiles()
|
|
|
|
def __execute(self):
|
|
try:
|
|
if self.__options.X_ind:
|
|
xmodmapfile = self.__homedir + "/.xmodmap"
|
|
xd = open(xmodmapfile,"w")
|
|
subprocess.Popen(["/usr/bin/xmodmap","-pke"],stdout=xd).wait()
|
|
xd.close()
|
|
|
|
self.__setup_sandboxrc(self.__options.wm)
|
|
|
|
cmds = [ '/usr/sbin/seunshare', "-t", self.__tmpdir, "-h", self.__homedir, "--", self.__execcon, "/usr/share/sandbox/sandboxX.sh" ]
|
|
rc = subprocess.Popen(cmds).wait()
|
|
return rc
|
|
|
|
if self.__mount:
|
|
cmds = [ '/usr/sbin/seunshare', "-t", self.__tmpdir, "-h", self.__homedir, "--", self.__execcon ] + self.__paths
|
|
rc = subprocess.Popen(cmds).wait()
|
|
return rc
|
|
|
|
selinux.setexeccon(self.__execcon)
|
|
rc = subprocess.Popen(self.__cmds).wait()
|
|
selinux.setexeccon(None)
|
|
return rc
|
|
|
|
finally:
|
|
for i in self.__paths:
|
|
if i not in SAVE_FILES:
|
|
continue
|
|
(dest, mtime) = SAVE_FILES[i]
|
|
if os.path.getmtime(dest) > mtime:
|
|
savefile(dest, i, self.__options.X_ind)
|
|
|
|
if self.__homedir and not self.__options.homedir:
|
|
shutil.rmtree(self.__homedir)
|
|
if self.__tmpdir and not self.__options.tmpdir:
|
|
shutil.rmtree(self.__tmpdir)
|
|
def main(self):
|
|
try:
|
|
self.__parse_options()
|
|
self.__gen_context()
|
|
self.__setup_dir()
|
|
return self.__execute()
|
|
except KeyboardInterrupt:
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
setup_sighandlers()
|
|
if selinux.is_selinux_enabled() != 1:
|
|
error_exit("Requires an SELinux enabled system")
|
|
|
|
try:
|
|
sandbox = Sandbox()
|
|
rc = sandbox.main()
|
|
except OSError, error:
|
|
error_exit(error.args[1])
|
|
except ValueError, error:
|
|
error_exit(error.args[0])
|
|
except KeyError, error:
|
|
error_exit(_("Invalid value %s") % error.args[0])
|
|
except IOError, error:
|
|
error_exit(error)
|
|
except KeyboardInterrupt:
|
|
rc = 0
|
|
|
|
sys.exit(rc)
|