Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Doc/library/shutil.rst
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,8 @@ Directory and files operations
.. index:: single: directory; deleting

Delete an entire directory tree; *path* must point to a directory (but not a
symbolic link to a directory). If *ignore_errors* is true, errors resulting
symbolic link to a directory, with or without a trailing path separator).
If *ignore_errors* is true, errors resulting
from failed removals will be ignored; if false or omitted, such errors are
handled by calling a handler specified by *onexc* or *onerror* or, if both
are omitted, exceptions are propagated to the caller.
Expand Down Expand Up @@ -367,6 +368,12 @@ Directory and files operations
Exceptions other than :exc:`OSError` and subclasses of :exc:`!OSError`
are now always propagated to the caller.

.. versionchanged:: next
A symbolic link to a directory is now also refused when *path* has a
trailing path separator, instead of deleting the contents of the
directory it points to. Error handlers receive *path* without
trailing separators.

.. attribute:: rmtree.avoids_symlink_attacks

Indicates whether the current platform and implementation provides a
Expand Down
10 changes: 10 additions & 0 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,16 @@ def rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None,
"""

sys.audit("shutil.rmtree", path, dir_fd)
# Strip trailing separators: "link/" names the target of a symlink, which
# would defeat the symlink check and delete the target's contents.
fspath = os.fspath(path)
drive, root, tail = os.path.splitroot(fspath)
seps = os.sep + (os.altsep or '')
if isinstance(fspath, bytes):
seps = os.fsencode(seps)
stripped = tail.rstrip(seps)
if stripped != tail:
path = drive + root + stripped
if ignore_errors:
def onexc(*args, **kwargs):
pass
Expand Down
36 changes: 36 additions & 0 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,42 @@ def onexc(*args):
self.assertEqual(errors[0][1], link)
self.assertIsInstance(errors[0][2], OSError)

@os_helper.skip_unless_symlink
def test_rmtree_fails_on_symlink_with_trailing_sep(self):
tmp = self.mkdtemp()
dir_ = os.path.join(tmp, 'dir')
os.mkdir(dir_)
create_file(os.path.join(dir_, 'file'), 'foo')
link = os.path.join(tmp, 'link')
os.symlink(dir_, link)
for path in (link + os.sep, link + os.sep * 2,
os.fsencode(link + os.sep)):
with self.subTest(path=path):
self.assertRaises(OSError, shutil.rmtree, path)
self.assertTrue(os.path.exists(os.path.join(dir_, 'file')))
self.assertTrue(os.path.lexists(link))
errors = []
def onexc(*args):
errors.append(args)
shutil.rmtree(link + os.sep, onexc=onexc)
self.assertEqual(len(errors), 1)
self.assertIs(errors[0][0], os.path.islink)
self.assertEqual(errors[0][1], link)
self.assertIsInstance(errors[0][2], OSError)
self.assertTrue(os.path.exists(os.path.join(dir_, 'file')))

def test_rmtree_works_with_trailing_sep(self):
tmp = self.mkdtemp()
victim = os.path.join(tmp, 'killme')
os.mkdir(victim)
create_file(os.path.join(victim, 'somefile'), 'foo')
shutil.rmtree(victim + os.sep)
self.assertFalse(os.path.lexists(victim))
os.mkdir(victim)
create_file(os.path.join(victim, 'somefile'), 'foo')
shutil.rmtree(os.fsencode(victim + os.sep * 2))
self.assertFalse(os.path.lexists(victim))

@os_helper.skip_unless_symlink
def test_rmtree_works_on_symlinks(self):
tmp = self.mkdtemp()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :func:`shutil.rmtree` deleting the contents of the target directory when
given a symbolic link to a directory with a trailing path separator. It now
fails like it does for the link without the separator.
Loading