542 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			542 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
	
| #!/usr/bin/python3 -Es
 | |
| # Authors: Dan Walsh <dwalsh@redhat.com>
 | |
| # Authors: Thomas Liu <tliu@fedoraproject.org>
 | |
| # 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
 | |
| import stat
 | |
| import sys
 | |
| import socket
 | |
| import random
 | |
| import fcntl
 | |
| import shutil
 | |
| import re
 | |
| import subprocess
 | |
| import selinux
 | |
| import signal
 | |
| from tempfile import mkdtemp
 | |
| import pwd
 | |
| import sepolicy
 | |
| 
 | |
| SEUNSHARE = "/usr/sbin/seunshare"
 | |
| SANDBOXSH = "/usr/share/sandbox/sandboxX.sh"
 | |
| PROGNAME = "policycoreutils"
 | |
| try:
 | |
|     import gettext
 | |
|     kwargs = {}
 | |
|     if sys.version_info < (3,):
 | |
|         kwargs['unicode'] = True
 | |
|     gettext.install(PROGNAME,
 | |
|                     localedir="/usr/share/locale",
 | |
|                     codeset='utf-8',
 | |
|                     **kwargs)
 | |
| except:
 | |
|     try:
 | |
|         import builtins
 | |
|         builtins.__dict__['_'] = str
 | |
|     except ImportError:
 | |
|         import __builtin__
 | |
|         __builtin__.__dict__['_'] = unicode
 | |
| 
 | |
| DEFAULT_WINDOWSIZE = "1000x700"
 | |
| 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, srcdir, dest):
 | |
|     import re
 | |
|     if file.startswith(srcdir):
 | |
|         dname = os.path.dirname(file)
 | |
|         bname = os.path.basename(file)
 | |
|         if dname == srcdir:
 | |
|             dest = dest + "/" + bname
 | |
|         else:
 | |
|             newdir = re.sub(srcdir, 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 as elist:
 | |
|             for e in elist.message:
 | |
|                 sys.stderr.write(e[2])
 | |
| 
 | |
|         SAVE_FILES[file] = (dest, os.path.getmtime(dest))
 | |
| 
 | |
| 
 | |
| def savefile(new, orig, X_ind):
 | |
|     copy = False
 | |
|     if(X_ind):
 | |
|         import gi
 | |
|         gi.require_version('Gtk', '3.0')
 | |
|         from gi.repository import Gtk
 | |
|         dlg = Gtk.MessageDialog(None, 0, Gtk.MessageType.INFO,
 | |
|                                 Gtk.ButtonsType.YES_NO,
 | |
|                                 _("Do you want to save changes to '%s' (Y/N): ") % orig)
 | |
|         dlg.set_title(_("Sandbox Message"))
 | |
|         dlg.set_position(Gtk.WindowPosition.MOUSE)
 | |
|         dlg.show_all()
 | |
|         rc = dlg.run()
 | |
|         dlg.destroy()
 | |
|         if rc == Gtk.ResponseType.YES:
 | |
|             copy = True
 | |
|     else:
 | |
|         try:
 | |
|             input = raw_input
 | |
|         except NameError:
 | |
|             pass
 | |
|         ans = 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 get_range():
 | |
|     try:
 | |
|         level = selinux.getcon_raw()[1].split(":")[4]
 | |
|         lowc, highc = level.split(".")
 | |
|         low = int(lowc[1:])
 | |
|         high = int(highc[1:]) + 1
 | |
|         if high - low == 0:
 | |
|             raise IndexError
 | |
| 
 | |
|         return low, high
 | |
|     except IndexError:
 | |
|         raise ValueError(_("User account must be setup with an MCS Range"))
 | |
| 
 | |
| 
 | |
| def gen_mcs():
 | |
|     low, high = get_range()
 | |
| 
 | |
|     level = None
 | |
|     ctr = 0
 | |
|     total = high - low
 | |
|     total = (total * (total - 1)) / 2
 | |
|     while ctr < total:
 | |
|         ctr += 1
 | |
|         i1 = random.randrange(low, high)
 | |
|         i2 = random.randrange(low, high)
 | |
|         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
 | |
|     if level:
 | |
|         return level
 | |
|     raise ValueError(_("Failed to find any unused category sets.  Consider a larger MCS range for this user."))
 | |
| 
 | |
| 
 | |
| 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:
 | |
|     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(SEUNSHARE):
 | |
|             raise ValueError(_("""
 | |
| %s is required for the action you want to perform.
 | |
| """) % SEUNSHARE)
 | |
| 
 | |
|     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)
 | |
|         if not os.path.exists(SEUNSHARE):
 | |
|             raise ValueError(_("""
 | |
| %s is required for the action you want to perform.
 | |
| """) % SEUNSHARE)
 | |
| 
 | |
|         if not os.path.exists(SANDBOXSH):
 | |
|             raise ValueError(_("""
 | |
| %s is required for the action you want to perform.
 | |
| """) % SANDBOXSH)
 | |
| 
 | |
|     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 as e:
 | |
|                 sys.stderr.write(str(e))
 | |
|             except TypeError as 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)
 | |
|             copyfile(f, "/var/tmp", self.__tmpdir)
 | |
| 
 | |
|     def __setup_sandboxrc(self, wm="/usr/bin/openbox"):
 | |
|         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 = self.__paths[0] + " "
 | |
|             for p in self.__paths[1:]:
 | |
|                 command += "'%s' " % p
 | |
|             fd.write("""#! /bin/sh
 | |
| #TITLE: %s
 | |
| # /usr/bin/test -r ~/.xmodmap && /usr/bin/xmodmap ~/.xmodmap
 | |
| %s &
 | |
| WM_PID=$!
 | |
| if which dbus-run-session >/dev/null 2>&1; then
 | |
|     dbus-run-session -- %s
 | |
| else
 | |
|     dbus-launch --exit-with-session %s
 | |
| fi
 | |
| kill -TERM $WM_PID  2> /dev/null
 | |
| """ % (command, wm, command, command))
 | |
|         fd.close()
 | |
|         os.chmod(execfile, 0o700)
 | |
| 
 | |
|     def usage(self, message=""):
 | |
|         error_exit("%s\n%s" % (self.__parser.usage, message))
 | |
| 
 | |
|     def __parse_options(self):
 | |
|         from optparse import OptionParser
 | |
|         types = ""
 | |
|         try:
 | |
|             types = _("""
 | |
| Policy defines the following types for use with the -t:
 | |
| \t%s
 | |
| """) % "\n\t".join(next(sepolicy.info(sepolicy.ATTRIBUTE, "sandbox_type"))['types'])
 | |
|         except StopIteration:
 | |
|             pass
 | |
| 
 | |
|         usage = _("""
 | |
| sandbox [-h] [-l level ] [-[X|M] [-H homedir] [-T tempdir]] [-I includefile ] [-W windowmanager ] [ -w windowsize ] [[-i file ] ...] [ -t type ] command
 | |
| 
 | |
| sandbox [-h] [-l level ] [-[X|M] [-H homedir] [-T tempdir]] [-I includefile ] [-W windowmanager ] [ -w windowsize ] [[-i file ] ...] [ -t type ] -S
 | |
| %s
 | |
| """) % types
 | |
| 
 | |
|         parser = OptionParser(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("-d", "--dpi",
 | |
|                           dest="dpi", action="store",
 | |
|                           help=_("dots per inch for X display"))
 | |
| 
 | |
|         parser.add_option("-S", "--session", action="store_true", dest="session",
 | |
|                           default=False, help=_("run complete desktop session within sandbox"))
 | |
| 
 | |
|         parser.add_option("-s", "--shred", action="store_true", dest="shred",
 | |
|                           default=False, help=_("Shred content before temporary directories are removed"))
 | |
| 
 | |
|         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", "--windowsize", dest="windowsize",
 | |
|                           type="string", default=DEFAULT_WINDOWSIZE,
 | |
|                           help="size of the sandbox window")
 | |
| 
 | |
|         parser.add_option("-W", "--windowmanager", dest="wm",
 | |
|                           type="string",
 | |
|                           default="/usr/bin/openbox",
 | |
|                           help=_("alternate window manager"))
 | |
| 
 | |
|         parser.add_option("-l", "--level", dest="level",
 | |
|                           help=_("MCS/MLS level for the sandbox"))
 | |
| 
 | |
|         parser.add_option("-C", "--capabilities",
 | |
|                           action="store_true", dest="usecaps", default=False,
 | |
|                           help="Allow apps requiring capabilities to run within the sandbox.")
 | |
| 
 | |
|         self.__parser = parser
 | |
| 
 | |
|         self.__options, cmds = parser.parse_args()
 | |
| 
 | |
|         if self.__options.X_ind:
 | |
|             self.setype = DEFAULT_X_TYPE
 | |
|         else:
 | |
|             try:
 | |
|                 next(sepolicy.info(sepolicy.TYPE, "sandbox_t"))
 | |
|             except StopIteration:
 | |
|                 raise ValueError(_("Sandbox Policy is not currently installed.\nYou need to install the selinux-policy-sandbox package in order to run this command"))
 | |
| 
 | |
|         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 self.__options.level:
 | |
|                 self.__homedir = self.__options.homedir
 | |
|                 self.__tmpdir = self.__options.tmpdir
 | |
| 
 | |
|             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:object_r:sandbox_file_t:%s" % (con[0], level)
 | |
| 
 | |
|     def __setup_dir(self):
 | |
|         selinux.setfscreatecon(self.__filecon)
 | |
|         if self.__options.homedir:
 | |
|             self.__homedir = self.__options.homedir
 | |
|         else:
 | |
|             self.__homedir = mkdtemp(dir="/tmp", prefix=".sandbox_home_")
 | |
| 
 | |
|         if self.__options.tmpdir:
 | |
|             self.__tmpdir = self.__options.tmpdir
 | |
|         else:
 | |
|             self.__tmpdir = mkdtemp(dir="/tmp", prefix=".sandbox_tmp_")
 | |
|         self.__copyfiles()
 | |
|         selinux.chcon(self.__homedir, self.__filecon, recursive=True)
 | |
|         selinux.chcon(self.__tmpdir, self.__filecon, recursive=True)
 | |
|         selinux.setfscreatecon(None)
 | |
| 
 | |
|     def __execute(self):
 | |
|         try:
 | |
|             cmds = [SEUNSHARE, "-Z", self.__execcon]
 | |
|             if self.__options.usecaps:
 | |
|                 cmds.append('-C')
 | |
|             if self.__mount:
 | |
|                 cmds += ["-t", self.__tmpdir, "-h", self.__homedir]
 | |
| 
 | |
|                 if self.__options.X_ind:
 | |
|                     if self.__options.dpi:
 | |
|                         dpi = self.__options.dpi
 | |
|                     else:
 | |
|                         import gi
 | |
|                         gi.require_version('Gtk', '3.0')
 | |
|                         from gi.repository import Gtk
 | |
|                         dpi = str(Gtk.Settings.get_default().props.gtk_xft_dpi / 1024)
 | |
| 
 | |
|                     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 += ["--", SANDBOXSH, self.__options.windowsize, dpi]
 | |
|                 else:
 | |
|                     cmds += ["--"] + self.__paths
 | |
|                 return subprocess.Popen(cmds).wait()
 | |
| 
 | |
|             pid = os.fork()
 | |
|             if pid == 0:
 | |
|                 rc = os.setsid()
 | |
|                 if rc:
 | |
|                     return rc
 | |
|                 selinux.setexeccon(self.__execcon)
 | |
|                 os.execv(self.__cmds[0], self.__cmds)
 | |
|             rc = os.waitpid(pid, 0)
 | |
|             return os.WEXITSTATUS(rc[1])
 | |
| 
 | |
|         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:
 | |
|                 if self.__options.shred:
 | |
|                     self.shred(self.__homedir)
 | |
|                 shutil.rmtree(self.__homedir)
 | |
|             if self.__tmpdir and not self.__options.tmpdir:
 | |
|                 if self.__options.shred:
 | |
|                     self.shred(self.__homedir)
 | |
|                 shutil.rmtree(self.__tmpdir)
 | |
| 
 | |
|     def shred(self, path):
 | |
|         for root, dirs, files in os.walk(path):
 | |
|             for f in files:
 | |
|                 dest = root + "/" + f
 | |
|                 subprocess.Popen(["/usr/bin/shred", dest]).wait()
 | |
| 
 | |
|     def main(self):
 | |
|         try:
 | |
|             self.__parse_options()
 | |
|             self.__gen_context()
 | |
|             if self.__mount:
 | |
|                 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 as error:
 | |
|         error_exit(error)
 | |
|     except ValueError as error:
 | |
|         error_exit(error.args[0])
 | |
|     except KeyError as error:
 | |
|         error_exit(_("Invalid value %s") % error.args[0])
 | |
|     except IOError as error:
 | |
|         error_exit(error)
 | |
|     except KeyboardInterrupt:
 | |
|         rc = 0
 | |
| 
 | |
|     sys.exit(rc)
 |