| #!/usr/bin/python3 -Es |
| # Authors: Dan Walsh <[email protected]> |
| # Authors: Thomas Liu <[email protected]> |
| # 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 = "selinux-sandbox" |
| try: |
| import gettext |
| kwargs = {} |
| if sys.version_info < (3,): |
| kwargs['unicode'] = True |
| t = gettext.translation(PROGNAME, |
| localedir="/usr/share/locale", |
| **kwargs, |
| fallback=True) |
| _ = t.gettext |
| 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 |
| self.__runuserdir = 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("-R", "--runuserdir", dest="runuserdir", |
| type="string", |
| action="callback", callback=self.__validdir, |
| help=_("alternate XDG_RUNTIME_DIR - /run/user/$UID - 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 |
| self.__runuserdir = self.__options.runuserdir |
| else: |
| if self.__options.level: |
| self.__homedir = self.__options.homedir |
| self.__tmpdir = self.__options.tmpdir |
| self.__runuserdir = self.__options.runuserdir |
| |
| 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_") |
| if self.__options.runuserdir: |
| self.__runuserdir = self.__options.runuserdir |
| else: |
| self.__runuserdir = mkdtemp(dir="/tmp", prefix=".sandbox_runuser_") |
| self.__copyfiles() |
| selinux.chcon(self.__homedir, self.__filecon, recursive=True) |
| selinux.chcon(self.__tmpdir, self.__filecon, recursive=True) |
| selinux.chcon(self.__runuserdir, 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, "-r", self.__runuserdir] |
| |
| 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 KeyboardInterrupt: |
| rc = 0 |
| |
| sys.exit(rc) |