[evolvis-commits] r18766: add a recursive file/directory ownership and permission bits changer…

mirabilos at evolvis.org mirabilos at evolvis.org
Thu May 16 16:46:59 CEST 2013


Author: mirabilos
Date: 2013-05-16 16:46:58 +0200 (Thu, 16 May 2013)
New Revision: 18766

Added:
   rch.py
Log:
add a recursive file/directory ownership and permission bits changer…

… which is hopefully *not* susceptible to CVE-2013-1423


Added: rch.py
===================================================================
--- rch.py	                        (rev 0)
+++ rch.py	2013-05-16 14:46:58 UTC (rev 18766)
@@ -0,0 +1,208 @@
+#!/usr/bin/env python
+# coding: utf-8
+#-
+# Copyright © 2013
+#   Thorsten Glaser <t.glaser at tarent.de>
+# Licenced under the AGPLv3.
+
+__version__ = """
+    $Id$
+"""
+
+__all__ = [
+    "RecursivelyChangeOwnershipAndPermission",
+]
+
+
+import os
+import stat
+
+class RecursivelyChangeOwnershipAndPermission(object):
+    u"""Change user/group and permission bits of a directory hierarchy.
+
+    To use, instantiate the class (once), then call the chownmod method.
+
+    This routine is supposed to be safe against symlink and (hard)link
+    attacks in group-writable (unsafe) directories, except a user who
+    can first hardlink a file to the basepath and then unlink it from
+    its original path will still have an attack vector (but in that
+    case, you have greater problems than that).
+
+    This routine will not even touch files that have hardlinks, for
+    the aforementioned security reasons. If your usage scenario relies
+    on them, put in additional checks (such as a whitelist of pathnames
+    combined with os.path.samestat’ing them).
+
+    """
+
+    def __init__(self, debug=False):
+        self._debug = debug
+        self._open_flags = os.O_RDONLY
+        try:
+            self._open_flags |= os.O_NOATIME
+        except Exception, e:
+            pass
+
+    def _d(self, s):
+        if self._debug:
+            print s
+
+    def chownmod(self, basepath, newuid, newgid, dirmode, filemode):
+        u"""Change user/group and permission bits of a directory hierarchy.
+
+        • basepath: name of a directory hierarchy to operate on
+        • newuid: new uid to set, or None
+        • newgid: new gid to set, or None
+        • dirmode: new permission bits to set on directories, or None
+        • filemode: new permission bits to set on regular files, or None
+
+        """
+
+        # this causes an OSError exception if basepath is not a directory
+        os.lstat(os.path.join(basepath, '.'))
+        self._d("Changing permissions on '%s'" % basepath)
+
+        self._need_chown = False
+        if newuid is None:
+            self._newuid = -1
+        else:
+            self._newuid = newuid
+            self._need_chown = True
+        if newgid is None:
+            self._newgid = -1
+        else:
+            self._newgid = newgid
+            self._need_chown = True
+        self._dirmode = dirmode
+        self._filemode = filemode
+
+        self._d("Uses chown? %s (%d, %d)" % \
+          (self._need_chown, self._newuid, self._newgid))
+        self._d("New modes: directory %04o files %04o" % ( \
+          self._dirmode is None and 077777 or self._dirmode,
+          self._filemode is None and 077777 or self._filemode))
+
+        self._achange(basepath)
+
+    def _rchange(self, basepath):
+        # iterate over all direct children of basepath
+        for item in os.listdir(basepath):
+            self._achange(os.path.join(basepath, item))
+
+    def _achange(self, fn):
+        self._d("Got '%s'" % fn)
+        stat_one = os.lstat(fn)
+        if stat.S_ISLNK(stat_one.st_mode):
+            # do not even attempt to follow symlinks
+            self._d(" => symlink, skipping")
+            return
+
+        target_mode = self._filemode
+        if stat.S_ISDIR(stat_one.st_mode):
+            target_mode = self._dirmode
+            # traverse directories recursively, depth first, post-order
+            self._d(" => directory, traversing")
+            self._rchange(fn)
+            self._d("<<< back to '%s'" % fn)
+        elif not stat.S_ISREG(stat_one.st_mode):
+            # not a directory or regular file, ignore
+            self._d(" => not a file (%o), skipping" % stat_one.st_mode)
+            return
+
+        do_shortcut_chown = True
+        do_shortcut_chmod = True
+        if target_mode is not None:
+            if stat.S_IMODE(stat_one.st_mode) != target_mode:
+                do_shortcut_chmod = False
+        if self._need_chown:
+            if self._newuid != -1 and stat_one.st_uid != self._newuid:
+                do_shortcut_chown = False
+            if self._newgid != -1 and stat_one.st_gid != self._newgid:
+                do_shortcut_chown = False
+        self._d(" => Ownership (%d, %d) and mode (%04o => %04o)" % \
+          (stat_one.st_uid, stat_one.st_gid, \
+          stat.S_IMODE(stat_one.st_mode), \
+          target_mode is None and 077777 or target_mode))
+        if do_shortcut_chown and do_shortcut_chmod:
+            # nothing to change
+            self._d(" => shortcutting, nothing to change")
+            return
+
+        # open whatever the file currently points to,
+        # to facilitate further security checks
+        fd = os.open(fn, self._open_flags)
+
+        # stat whatever we actually opened
+        stat_two = os.fstat(fd)
+        if not os.path.samestat(stat_one, stat_two):
+            # the file changed under our arse, ignore
+            os.close(fd)
+            self._d(" !!! file changed, skipping")
+            return
+
+        # ensure that the file is not hardlinked at all
+        if stat.S_ISREG(stat_two.st_mode) and stat_two.st_nlink != 1:
+            os.close(fd)
+            self._d(" => file has %d links, skipping" % stat_two.st_nlink)
+            return
+
+        # now change ownership and permission bits, but
+        # on whatever we opened, not on whatever the
+        # pathname currently points to
+        if not do_shortcut_chown:
+            self._d(" -> changing ownership to (%d, %d)" % \
+              (self._newuid, self._newgid))
+            os.fchown(fd, self._newuid, self._newgid)
+        if not do_shortcut_chmod:
+            self._d(" -> changing permissions to %04o" % target_mode)
+            os.fchmod(fd, target_mode)
+
+        # can close the file again
+        os.close(fd)
+        return
+
+
+if __name__ == "__main__":
+    import sys
+    try:
+        if len(sys.argv) != 6:
+            raise ValueError("invalid number of arguments: %d" % \
+              (len(sys.argv) - 1))
+        basepath = sys.argv[1]
+        if sys.argv[2] == '-':
+            newuid = None
+        else:
+            import pwd
+            try:
+                newuid = pwd.getpwnam(sys.argv[2]).pw_uid
+            except Exception, e:
+                newuid = pwd.getpwuid(int(sys.argv[2], 0)).pw_uid
+        if sys.argv[3] == '-':
+            newgid = None
+        else:
+            import grp
+            try:
+                newgid = grp.getgrnam(sys.argv[3]).gr_gid
+            except Exception, e:
+                newgid = grp.getgrgid(int(sys.argv[3], 0)).gr_gid
+        if sys.argv[4] == '-':
+            dirmode = None
+        else:
+            dirmode = int(sys.argv[4], 8)
+        if sys.argv[5] == '-':
+            filemode = None
+        else:
+            filemode = int(sys.argv[5], 8)
+    except Exception, e:
+        print "Syntax: %s 'path' user group dirmode filemode" % \
+          (sys.argv[0] or "rch.py")
+        raise e
+        sys.exit(255)
+    foo = RecursivelyChangeOwnershipAndPermission(True)
+    try:
+        foo.chownmod(basepath, newuid, newgid, dirmode, filemode)
+    except Exception, e:
+        print "Error occurred during setting modes"
+        raise e
+        sys.exit(1)
+    sys.exit(0)


Property changes on: rch.py
___________________________________________________________________
Added: svn:keywords
   + Id



More information about the evolvis-commits mailing list