Skip to content
Open
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
16 changes: 12 additions & 4 deletions Lib/pathlib/_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,16 +268,24 @@ def ensure_distinct_paths(source, target):
# Note: there is no straightforward, foolproof algorithm to determine
# if one directory is within another (a particularly perverse example
# would be a single network share mounted in one location via NFS, and
# in another location via CIFS), so we simply checks whether the
# other path is lexically equal to, or within, this path.
# in another location via CIFS). Resolve when possible so relative vs
# absolute paths and symlink aliases are compared like shutil._destinsrc
# (realpath); otherwise fall back to a lexical check.
source_orig, target_orig = source, target
try:
source = source.resolve(strict=False)
target = target.resolve(strict=False)
except (AttributeError, OSError):
# No resolve(), or realpath failed.
pass
if source == target:
err = OSError(EINVAL, "Source and target are the same path")
elif source in target.parents:
err = OSError(EINVAL, "Source path is a parent of target path")
else:
return
err.filename = vfspath(source)
err.filename2 = vfspath(target)
err.filename = vfspath(source_orig)
err.filename2 = vfspath(target_orig)
raise err


Expand Down
43 changes: 43 additions & 0 deletions Lib/test/test_pathlib/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,49 @@ def test_copy_directory_symlink_into_itself(self):
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
self.assertFalse(target.exists())

def test_copy_dir_into_itself_relative_source_absolute_target(self):
with os_helper.change_cwd(self.base):
source = self.cls('dirC')
target = source.resolve() / 'copyC'
self.assertRaises(OSError, source.copy, target)
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
self.assertFalse(target.exists())

def test_copy_dir_into_itself_absolute_source_relative_target(self):
with os_helper.change_cwd(self.base):
source = self.cls(self.base) / 'dirC'
target = self.cls('dirC') / 'copyC'
self.assertRaises(OSError, source.copy, target)
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
self.assertFalse((source / 'copyC').exists())

@needs_symlinks
def test_copy_dir_into_itself_via_symlink(self):
base = self.cls(self.base)
source = base / 'dirC'
link = base / 'linkToDirC'
link.symlink_to(source, target_is_directory=True)
target = link / 'copyC'
self.assertRaises(OSError, source.copy, target)
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
self.assertFalse((source / 'copyC').exists())

def test_copy_dir_into_itself_when_no_resolve(self):
base = self.cls(self.base)
source = base / 'dirC'
target = source / 'copyC'
with mock.patch.object(self.cls, 'resolve', side_effect=AttributeError):
self.assertRaises(OSError, source.copy, target)
self.assertFalse(target.exists())

def test_copy_dir_into_itself_when_resolve_raises_oserror(self):
base = self.cls(self.base)
source = base / 'dirC'
target = source / 'copyC'
with mock.patch.object(self.cls, 'resolve', side_effect=OSError):
self.assertRaises(OSError, source.copy, target)
self.assertFalse(target.exists())

@needs_symlinks
def test_copy_directory_symlink_to_existing_symlink(self):
base = self.cls(self.base)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:meth:`pathlib.Path.copy` now resolves paths when checking whether the
destination is inside the source directory, preventing bypasses via
relative/absolute path aliases and symlinks.
Loading