diff --git a/Lib/tarfile.py b/Lib/tarfile.py index a4f9ce3311f6dad..f37a05bd67703b3 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -842,7 +842,13 @@ def _get_filtered_attrs(member, dest_path, for_data=True): name = new_attrs['name'] = normalized 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 @@ -897,7 +903,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 55f052d0504d67b..3dc69b69ae3dcaf 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -4102,6 +4102,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') @@ -4134,6 +4142,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') + def test_parent_dir_out_and_back(self): # Test a member that leaves the destination and comes back. # The containment check looks at the resolved path, which stays @@ -4404,6 +4432,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 000000000000000..06ec380127e9b5c --- /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.