#4489: Fix usage of fd-based functions to new api introduced earlier today

Also add an explicit test for safe implementation usage on supported platforms.

As a side effect, this commit adds a module-level attribute 'rmtree_is_safe'
which offers introspection whether the current rmtree implementation is safe
against symlink attacks.
This commit is contained in:
Hynek Schlawack 2012-06-23 20:28:32 +02:00
parent 1641cea02b
commit 2100b42317
3 changed files with 44 additions and 17 deletions

View File

@ -195,9 +195,9 @@ Directory and files operations
The default :func:`rmtree` function is susceptible to a symlink attack: The default :func:`rmtree` function is susceptible to a symlink attack:
given proper timing and circumstances, attackers can use it to delete given proper timing and circumstances, attackers can use it to delete
files they wouldn't be able to access otherwise. Thus -- on platforms files they wouldn't be able to access otherwise. Thus -- on platforms
that support the necessary fd-based functions :func:`os.openat` and that support the necessary fd-based functions -- a safe version of
:func:`os.unlinkat` -- a safe version of :func:`rmtree` is used, which :func:`rmtree` is used, which isn't vulnerable. In this case
isn't vulnerable. :data:`rmtree_is_safe` is set to True.
If *onerror* is provided, it must be a callable that accepts three If *onerror* is provided, it must be a callable that accepts three
parameters: *function*, *path*, and *excinfo*. parameters: *function*, *path*, and *excinfo*.
@ -210,8 +210,15 @@ Directory and files operations
.. versionchanged:: 3.3 .. versionchanged:: 3.3
Added a safe version that is used automatically if platform supports Added a safe version that is used automatically if platform supports
the fd-based functions :func:`os.openat` and :func:`os.unlinkat`. fd-based functions.
.. data:: rmtree_is_safe
Indicates whether the current platform and implementation has a symlink
attack-proof version of :func:`rmtree`. Currently this is only true for
platforms supporting fd-based directory access functions.
.. versionadded:: 3.3
.. function:: move(src, dst) .. function:: move(src, dst)

View File

@ -362,9 +362,9 @@ def _rmtree_unsafe(path, onerror):
_rmtree_unsafe(fullname, onerror) _rmtree_unsafe(fullname, onerror)
else: else:
try: try:
os.remove(fullname) os.unlink(fullname)
except os.error: except os.error:
onerror(os.remove, fullname, sys.exc_info()) onerror(os.unlink, fullname, sys.exc_info())
try: try:
os.rmdir(path) os.rmdir(path)
except os.error: except os.error:
@ -374,21 +374,21 @@ def _rmtree_unsafe(path, onerror):
def _rmtree_safe_fd(topfd, path, onerror): def _rmtree_safe_fd(topfd, path, onerror):
names = [] names = []
try: try:
names = os.flistdir(topfd) names = os.listdir(topfd)
except os.error: except os.error:
onerror(os.flistdir, path, sys.exc_info()) onerror(os.listdir, path, sys.exc_info())
for name in names: for name in names:
fullname = os.path.join(path, name) fullname = os.path.join(path, name)
try: try:
orig_st = os.fstatat(topfd, name) orig_st = os.stat(name, dir_fd=topfd)
mode = orig_st.st_mode mode = orig_st.st_mode
except os.error: except os.error:
mode = 0 mode = 0
if stat.S_ISDIR(mode): if stat.S_ISDIR(mode):
try: try:
dirfd = os.openat(topfd, name, os.O_RDONLY) dirfd = os.open(name, os.O_RDONLY, dir_fd=topfd)
except os.error: except os.error:
onerror(os.openat, fullname, sys.exc_info()) onerror(os.open, fullname, sys.exc_info())
else: else:
try: try:
if os.path.samestat(orig_st, os.fstat(dirfd)): if os.path.samestat(orig_st, os.fstat(dirfd)):
@ -397,21 +397,22 @@ def _rmtree_safe_fd(topfd, path, onerror):
os.close(dirfd) os.close(dirfd)
else: else:
try: try:
os.unlinkat(topfd, name) os.unlink(name, dir_fd=topfd)
except os.error: except os.error:
onerror(os.unlinkat, fullname, sys.exc_info()) onerror(os.unlink, fullname, sys.exc_info())
try: try:
os.rmdir(path) os.rmdir(path)
except os.error: except os.error:
onerror(os.rmdir, path, sys.exc_info()) onerror(os.rmdir, path, sys.exc_info())
_use_fd_functions = hasattr(os, 'openat') and hasattr(os, 'unlinkat') rmtree_is_safe = _use_fd_functions = (os.unlink in os.supports_dir_fd and
os.open in os.supports_dir_fd)
def rmtree(path, ignore_errors=False, onerror=None): def rmtree(path, ignore_errors=False, onerror=None):
"""Recursively delete a directory tree. """Recursively delete a directory tree.
If ignore_errors is set, errors are ignored; otherwise, if onerror If ignore_errors is set, errors are ignored; otherwise, if onerror
is set, it is called to handle the error with arguments (func, is set, it is called to handle the error with arguments (func,
path, exc_info) where func is os.listdir, os.remove, or os.rmdir; path, exc_info) where func is platform and implementation dependent;
path is the argument to that function that caused it to fail; and path is the argument to that function that caused it to fail; and
exc_info is a tuple returned by sys.exc_info(). If ignore_errors exc_info is a tuple returned by sys.exc_info(). If ignore_errors
is false and onerror is None, an exception is raised. is false and onerror is None, an exception is raised.

View File

@ -159,8 +159,7 @@ class TestShutil(unittest.TestCase):
# at os.listdir. The first failure may legally # at os.listdir. The first failure may legally
# be either. # be either.
if 0 <= self.errorState < 2: if 0 <= self.errorState < 2:
if (func is os.remove or if func is os.unlink:
hasattr(os, 'unlinkat') and func is os.unlinkat):
self.assertIn(arg, [self.child_file_path, self.child_dir_path]) self.assertIn(arg, [self.child_file_path, self.child_dir_path])
else: else:
if self.errorState == 1: if self.errorState == 1:
@ -486,6 +485,26 @@ class TestShutil(unittest.TestCase):
shutil.copyfile(link, dst) shutil.copyfile(link, dst)
self.assertFalse(os.path.islink(dst)) self.assertFalse(os.path.islink(dst))
def test_rmtree_uses_safe_fd_version_if_available(self):
if os.unlink in os.supports_dir_fd and os.open in os.supports_dir_fd:
self.assertTrue(shutil._use_fd_functions)
self.assertTrue(shutil.rmtree_is_safe)
tmp_dir = self.mkdtemp()
d = os.path.join(tmp_dir, 'a')
os.mkdir(d)
try:
real_rmtree = shutil._rmtree_safe_fd
class Called(Exception): pass
def _raiser(*args, **kwargs):
raise Called
shutil._rmtree_safe_fd = _raiser
self.assertRaises(Called, shutil.rmtree, d)
finally:
shutil._rmtree_safe_fd = real_rmtree
else:
self.assertFalse(shutil._use_fd_functions)
self.assertFalse(shutil.rmtree_is_safe)
def test_rmtree_dont_delete_file(self): def test_rmtree_dont_delete_file(self):
# When called on a file instead of a directory, don't delete it. # When called on a file instead of a directory, don't delete it.
handle, path = tempfile.mkstemp() handle, path = tempfile.mkstemp()