diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -82,13 +82,15 @@ .. versionadded:: 3.4 -.. function:: copymode(src, dst, *, follow_symlinks=True) +.. function:: copymode(src, dst, *, follow_symlinks=True, preserve_sbits=True) Copy the permission bits from *src* to *dst*. The file contents, owner, and group are unaffected. *src* and *dst* are path names given as strings. If *follow_symlinks* is false, and both *src* and *dst* are symbolic links, :func:`copymode` will attempt to modify the mode of *dst* itself (rather than the file it points to). This functionality is not available on every + If *preserve_sbits* is false, setuid, setgid and sticky bit are not copied + to *dst*. platform; please see :func:`copystat` for more information. If :func:`copymode` cannot modify symbolic links on the local platform, and it is asked to do so, it will do nothing and return. @@ -96,7 +98,11 @@ .. versionchanged:: 3.3 Added *follow_symlinks* argument. -.. function:: copystat(src, dst, *, follow_symlinks=True) + .. versionchanged:: XXX + Added *preserve_sbits* argument. + + +.. function:: copystat(src, dst, *, follow_symlinks=True, preserve_sbits=True) Copy the permission bits, last access time, last modification time, and flags from *src* to *dst*. On Linux, :func:`copystat` also copies the @@ -110,6 +116,9 @@ *src* symbolic link, and writing the information to the *dst* symbolic link. + If *preserve_sbits* is false, setuid, setgid and sticky bit are not copied + to *dst*. + .. note:: Not all platforms provide the ability to examine and @@ -137,10 +146,16 @@ Please see :data:`os.supports_follow_symlinks` for more information. + The setuid, setgid and sticky bits aren't copied to prevent privilege + escalations. + .. versionchanged:: 3.3 Added *follow_symlinks* argument and support for Linux extended attributes. -.. function:: copy(src, dst, *, follow_symlinks=True) + .. versionchanged:: XXX + Added *preserve_sbits* argument. + +.. function:: copy(src, dst, *, follow_symlinks=True, preserve_sbits=True) Copies the file *src* to the file or directory *dst*. *src* and *dst* should be strings. If *dst* specifies a directory, the file will be @@ -152,6 +167,9 @@ is true and *src* is a symbolic link, *dst* will be a copy of the file *src* refers to. + If *preserve_sbits* is false, setuid, setgid and sticky bit are not copied + to *dst*. + :func:`copy` copies the file data and the file's permission mode (see :func:`os.chmod`). Other metadata, like the file's creation and modification times, is not preserved. @@ -162,7 +180,10 @@ Added *follow_symlinks* argument. Now returns path to the newly created file. -.. function:: copy2(src, dst, *, follow_symlinks=True) + .. versionchanged:: XXX + Added *preserve_sbits* argument. + +.. function:: copy2(src, dst, *, follow_symlinks=True, preserve_sbits=True) Identical to :func:`~shutil.copy` except that :func:`copy2` also attempts to preserve all file metadata. @@ -175,6 +196,9 @@ unavailable, :func:`copy2` will preserve all the metadata it can; :func:`copy2` never returns failure. + If *preserve_sbits* is false, setuid, setgid and sticky bit are not copied + to *dst*. + :func:`copy2` uses :func:`copystat` to copy the file metadata. Please see :func:`copystat` for more information about platform support for modifying symbolic link metadata. @@ -184,6 +208,9 @@ file system attributes too (currently Linux only). Now returns path to the newly created file. + .. versionchanged:: XXX + Added *preserve_sbits* argument. + .. function:: ignore_patterns(\*patterns) This factory function creates a function that can be used as a callable for diff --git a/Lib/shutil.py b/Lib/shutil.py --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -109,7 +109,10 @@ copyfileobj(fsrc, fdst) return dst -def copymode(src, dst, *, follow_symlinks=True): +# complementary bit mask of harmful bits (suid, sgid, sticky) +_REMOVE_SBITS_MASK = ~ (stat.S_ISUID | stat.S_ISGID | stat.S_ISVTX) + +def copymode(src, dst, *, follow_symlinks=True, preserve_sbits=True): """Copy mode bits from src to dst. If follow_symlinks is not set, symlinks aren't followed if and only @@ -128,7 +131,11 @@ return st = stat_func(src) - chmod_func(dst, stat.S_IMODE(st.st_mode)) + mode = stat.S_IMODE(st.st_mode) + if not preserve_sbits: + # remove setuid, setgid and sticky bits + mode &= _REMOVE_SBITS_MASK + chmod_func(dst, mode) if hasattr(os, 'listxattr'): def _copyxattr(src, dst, *, follow_symlinks=True): @@ -157,7 +164,7 @@ def _copyxattr(*args, **kwargs): pass -def copystat(src, dst, *, follow_symlinks=True): +def copystat(src, dst, *, follow_symlinks=True, preserve_sbits=True): """Copy all stat info (mode bits, atime, mtime, flags) from src to dst. If the optional flag `follow_symlinks` is not set, symlinks aren't followed if and @@ -184,6 +191,9 @@ st = lookup("stat")(src, follow_symlinks=follow) mode = stat.S_IMODE(st.st_mode) + if not preserve_sbits: + # remove setuid, setgid and sticky bits + mode &= _REMOVE_SBITS_MASK lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns), follow_symlinks=follow) try: @@ -211,7 +221,7 @@ raise _copyxattr(src, dst, follow_symlinks=follow) -def copy(src, dst, *, follow_symlinks=True): +def copy(src, dst, *, follow_symlinks=True, preserve_sbits=True): """Copy data and mode bits ("cp src dst"). Return the file's destination. The destination may be a directory. @@ -226,10 +236,11 @@ if os.path.isdir(dst): dst = os.path.join(dst, os.path.basename(src)) copyfile(src, dst, follow_symlinks=follow_symlinks) - copymode(src, dst, follow_symlinks=follow_symlinks) + copymode(src, dst, follow_symlinks=follow_symlinks, + preserve_sbits=preserve_sbits) return dst -def copy2(src, dst, *, follow_symlinks=True): +def copy2(src, dst, *, follow_symlinks=True, preserve_sbits=True): """Copy data and all stat info ("cp -p src dst"). Return the file's destination." @@ -242,7 +253,8 @@ if os.path.isdir(dst): dst = os.path.join(dst, os.path.basename(src)) copyfile(src, dst, follow_symlinks=follow_symlinks) - copystat(src, dst, follow_symlinks=follow_symlinks) + copystat(src, dst, follow_symlinks=follow_symlinks, + preserve_sbits=preserve_sbits) return dst def ignore_patterns(*patterns): diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1287,6 +1287,31 @@ rv = shutil.copytree(src_dir, dst_dir) self.assertEqual(['foo'], os.listdir(rv)) + @unittest.skipUnless(os.name == "posix", "Requires POSIX compatible OS") + def test_copy_preserve_sbits(self): + src_dir = self.mkdtemp() + src_file = os.path.join(src_dir, 'foo') + write_file(src_file, 'foo') + dst_file = os.path.join(src_dir, 'bar') + harmful_mode = stat.S_IRUSR | stat.S_IXUSR | stat.S_ISUID + harmless_mode = stat.S_IRUSR | stat.S_IXUSR + + # set mode and verify + os.chmod(src_file, harmful_mode) + mode = stat.S_IMODE(os.stat(src_file).st_mode) + self.assertTrue(oct(mode), oct(harmful_mode)) + + # check that copy does not preserve harmful bits + shutil.copy(src_file, dst_file, preserve_sbits=False) + mode = stat.S_IMODE(os.stat(dst_file).st_mode) + self.assertEqual(oct(mode), oct(harmless_mode)) + os.unlink(dst_file) + + # check that copy2 does not preserve harmful bits + shutil.copy2(src_file, dst_file, preserve_sbits=False) + mode = stat.S_IMODE(os.stat(dst_file).st_mode) + self.assertEqual(oct(mode), oct(harmless_mode)) + class TestWhich(unittest.TestCase):