[issue4489] shutil.rmtree is vulnerable to a symlink attack

Charles-François Natali report at bugs.python.org
Sat Nov 5 00:58:30 CET 2011


Charles-François Natali <neologix at free.fr> added the comment:

There's a race:
"""
--- Lib/shutil.py       2011-11-05 00:11:05.745221315 +0100
+++ Lib/shutil.py.new   2011-11-05 00:11:01.445220324 +0100
@@ -307,6 +307,7 @@
        try:
            mode = os.fstatat(dirfd, name, os.AT_SYMLINK_NOFOLLOW).st_mode
         except os.error:
             mode = 0
         if stat.S_ISDIR(mode):
+            input("press enter")
             newfd = os.openat(dirfd, name, os.O_RDONLY)
             _rmtree_safe(newfd, ignore_errors, onerror)
             try:
"""

$ rm -rf /tmp/target
$ mkdir -p /tmp/target/etc
$ ./python -c "import shutil; shutil.rmtree('/tmp/target')"
press enter^Z
[1]+  Stopped                 ./python -c "import shutil; shutil.rmtree('/tmp/target')"
$ rm -r /tmp/target/etc; ln -s /etc /tmp/target/
$ fg
./python -c "import shutil; shutil.rmtree('/tmp/target')"

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/cf/python/cpython/Lib/shutil.py", line 290, in rmtree
    _rmtree_safe(fd, ignore_errors, onerror)
  File "/home/cf/python/cpython/Lib/shutil.py", line 314, in _rmtree_safe
    _rmtree_safe(newfd, ignore_errors, onerror)
  File "/home/cf/python/cpython/Lib/shutil.py", line 323, in _rmtree_safe
    onerror(os.unlinkat, (dirfd, name), sys.exc_info())
  File "/home/cf/python/cpython/Lib/shutil.py", line 321, in _rmtree_safe
    os.unlinkat(dirfd, name)
PermissionError: [Errno 13] Permission denied
[52334 refs]

"""
openat(3, "etc", O_RDONLY|O_LARGEFILE)  = 4
dup(4)                                  = 5
fstat64(5, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
fcntl64(5, F_GETFL)                     = 0x8000 (flags O_RDONLY|O_LARGEFILE)
fcntl64(5, F_SETFD, FD_CLOEXEC)         = 0
getdents64(5, /* 162 entries */, 32768) = 5176
getdents64(5, /* 0 entries */, 32768)   = 0
close(5)                                = 0
fstatat64(4, "passwd", {st_mode=S_IFREG|0644, st_size=980, ...}, AT_SYMLINK_NOFOLLOW) = 0
unlinkat(4, "passwd", 0)                = -1 EACCES (Permission denied)
"""

You should use the lstat/open/fstat idiom.
Also, here:
"""
            mode1 = os.lstat(path).st_mode
            if stat.S_ISLNK(mode1):
                raise OSError("Cannot call rmtree on a symbolic link")
        except OSError:
            onerror(os.lstat, path, sys.exc_info())
            # can't continue even if onerror hook returns
            return
        fd = os.open(path, os.O_RDONLY)
        try:
            mode2 = os.fstat(fd).st_mode
            if mode1 != mode2:
                raise OSError("Target changed")
"""

You check that path is not a symlink, then you open it, perform fstat on it, and check that the mode is the same.
But if someone replaces path by a symlink to a directory with the same mode, then rmtree won't catch this. You should also compare st_dev and st_ino to make sure we're dealing with the same file.

One more thing :-)
"""
        fd = os.open(path, os.O_RDONLY)
        try:
            mode2 = os.fstat(fd).st_mode
            if mode1 != mode2:
                raise OSError("Target changed")
        except OSError:
            onerror(os.fstat, fd, sys.exc_info())
            # can't continue if target has changed
            return
"""

Here `fd` is not closed (there might be other places leaking FD).

Finally, since writting a such code is tricky, what do you - all - think of making this a generic walker method that would take as argument the methods to call on a directory and on a file (or link), so that we could reuse it to write chmodtree(), chowntree() and friends?

----------
nosy: +neologix

_______________________________________
Python tracker <report at bugs.python.org>
<http://bugs.python.org/issue4489>
_______________________________________


More information about the Python-bugs-list mailing list