Skip to content

Commit

Permalink
Add Path.delete.avoids_symlink_attacks
Browse files Browse the repository at this point in the history
  • Loading branch information
barneygale committed Aug 3, 2024
1 parent e9835e0 commit b84b0e0
Show file tree
Hide file tree
Showing 4 changed files with 13 additions and 6 deletions.
12 changes: 7 additions & 5 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1670,11 +1670,13 @@ Copying, renaming and deleting

.. note::

On platforms that lack the necessary file descriptor-based functions,
:func:`~Path.delete` implementation is susceptible to a symlink attack:
given proper timing and circumstances, attackers can manipulate symlinks
on the filesystem to delete files they would not be able to access
otherwise.
When deleting non-empty directories on platforms that lack the necessary
file descriptor-based functions, the :meth:`Path.delete` implementation
is susceptible to a symlink attack: given proper timing and
circumstances, attackers can manipulate symlinks on the filesystem to
delete files they would not be able to access otherwise. Applications
can use the :data:`Path.delete.avoids_symlink_attacks` method attribute
to determine whether the implementation is immune to this attack.

.. versionadded:: 3.14

Expand Down
1 change: 1 addition & 0 deletions Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,7 @@ def on_error(err):
except OSError as err:
err.filename = str(self)
on_error(err)
delete.avoids_symlink_attacks = False

def owner(self, *, follow_symlinks=True):
"""
Expand Down
3 changes: 2 additions & 1 deletion Lib/pathlib/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import operator
import os
import posixpath
import shutil
import sys
from glob import _StringGlobber
from itertools import chain
Expand Down Expand Up @@ -846,7 +847,6 @@ def delete(self, ignore_errors=False, on_error=None):
def onexc(func, filename, err):
err.filename = filename
on_error(err)
import shutil
shutil.rmtree(str(self), ignore_errors, onexc=onexc)
else:
try:
Expand All @@ -858,6 +858,7 @@ def onexc(func, filename, err):
else:
raise

delete.avoids_symlink_attacks = shutil.rmtree.avoids_symlink_attacks

def rename(self, target):
"""
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/test_pathlib/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,7 @@ def test_group_no_follow_symlinks(self):

def test_delete_uses_safe_fd_version_if_available(self):
if delete_use_fd_functions:
self.assertTrue(self.cls.delete.avoids_symlink_attacks)
d = self.cls(self.base, 'a')
d.mkdir()
try:
Expand All @@ -879,6 +880,8 @@ def _raiser(*args, **kwargs):
self.assertRaises(Called, d.delete)
finally:
os.open = real_open
else:
self.assertFalse(self.cls.delete.avoids_symlink_attacks)

@unittest.skipIf(sys.platform[:6] == 'cygwin',
"This test can't be run on Cygwin (issue #1071513).")
Expand Down

0 comments on commit b84b0e0

Please sign in to comment.