diff --git a/Lib/tarfile.py b/Lib/tarfile.py index d12bd15aa2d231..a7fee40d573538 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -835,7 +835,13 @@ def _get_filtered_attrs(member, dest_path, for_data=True): # Ensure we stay in the destination target_path = os.path.realpath(os.path.join(dest_path, name), strict=os.path.ALLOW_MISSING) - if os.path.commonpath([target_path, dest_path]) != dest_path: + try: + outside_destination = (os.path.commonpath([target_path, dest_path]) + != dest_path) + except ValueError: + # commonpath() raises for paths on different drives on Windows. + outside_destination = True + if outside_destination: raise OutsideDestinationError(member, target_path) # Limit permissions (no high bits, and go-w) mode = member.mode @@ -890,7 +896,13 @@ def _get_filtered_attrs(member, dest_path, for_data=True): target_path = os.path.join(dest_path, normalized) target_path = os.path.realpath(target_path, strict=os.path.ALLOW_MISSING) - if os.path.commonpath([target_path, dest_path]) != dest_path: + try: + outside_destination = ( + os.path.commonpath([target_path, dest_path]) != dest_path) + except ValueError: + # commonpath() raises for paths on different drives on Windows. + outside_destination = True + if outside_destination: raise LinkOutsideDestinationError(member, target_path) return new_attrs diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index c86bcb79eb85d8..7bf6c1cd2e0c51 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -3993,6 +3993,14 @@ def expect_exception(self, exc_type, message_re='.'): self.reraise_exception = False return self.raised_exception + def _make_drive_relative_path(self, name): + dest_drive = os.path.splitdrive(os.path.realpath( + self.destdir, strict=os.path.ALLOW_MISSING))[0] + for drive in "ZYXWVUTSRQPONMLKJIHGFEDCBA": + if drive.upper() != dest_drive.rstrip(":").upper(): + return f"{drive}:{name}" + raise AssertionError("could not find a different drive") + def test_benign_file(self): with ArchiveMaker() as arc: arc.add('benign.txt') @@ -4025,6 +4033,26 @@ def test_absolute(self): tarfile.AbsolutePathError, """['"].*escaped.evil['"] has an absolute path""") + @unittest.skipIf(os.name != 'nt', 'Windows-specific drive-relative path') + def test_drive_relative_member_path(self): + # Drive-relative paths such as "Z:evil" are not absolute, but + # ntpath.commonpath() raises ValueError for them. + member_name = self._make_drive_relative_path('evil') + with ArchiveMaker() as arc: + arc.add('before') + arc.add(member_name) + arc.add('after') + + for filter in 'tar', 'data': + with self.subTest(filter=filter, errorlevel=1): + with self.check_context(arc.open(errorlevel=1), filter): + self.expect_exception(tarfile.OutsideDestinationError) + + with self.subTest(filter=filter, errorlevel=0): + with self.check_context(arc.open(errorlevel=0), filter): + self.expect_file('before') + self.expect_file('after') + @symlink_test def test_parent_symlink(self): # Test interplaying symlinks @@ -4281,6 +4309,28 @@ def test_absolute_hardlink(self): tarfile.AbsoluteLinkError, "'parent' is a link to an absolute path") + @unittest.skipIf(os.name != 'nt', 'Windows-specific drive-relative path') + @support.subTests('link_type', (tarfile.SYMTYPE, tarfile.LNKTYPE)) + def test_data_filter_drive_relative_link_path(self, link_type): + linkname = self._make_drive_relative_path('evil') + link_kwargs = {} + if link_type == tarfile.SYMTYPE: + link_kwargs['symlink_to'] = linkname + else: + link_kwargs['hardlink_to'] = linkname + + with ArchiveMaker() as arc: + arc.add('before') + arc.add('link', **link_kwargs) + arc.add('after') + + with self.check_context(arc.open(errorlevel=1), 'data'): + self.expect_exception(tarfile.LinkOutsideDestinationError) + + with self.check_context(arc.open(errorlevel=0), 'data'): + self.expect_file('before') + self.expect_file('after') + @symlink_test def test_sly_relative0(self): # Inspired by 'relative0' in jwilk/traversal-archives diff --git a/Misc/NEWS.d/next/Library/2026-07-31-16-30-00.bpo-0.xz7oaj.rst b/Misc/NEWS.d/next/Library/2026-07-31-16-30-00.bpo-0.xz7oaj.rst new file mode 100644 index 00000000000000..06ec380127e9b5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-31-16-30-00.bpo-0.xz7oaj.rst @@ -0,0 +1,3 @@ +On Windows, :mod:`tarfile` extraction filters now report drive-relative +member names and link targets using structured filter errors instead of +allowing :exc:`ValueError` from :func:`os.path.commonpath` to escape.