diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst index d2152be86de1c91..e67bf6ae029c301 100644 --- a/Doc/library/tarfile.rst +++ b/Doc/library/tarfile.rst @@ -1056,6 +1056,11 @@ reused in custom filters: - :ref:`Refuse ` to extract files whose absolute path (after following symlinks) would end up outside the destination. This raises :class:`~tarfile.OutsideDestinationError`. + - :ref:`Refuse ` to extract links (hard or soft) + whose name refers to the destination directory itself. + This raises :class:`~tarfile.OutsideDestinationError`. + Note that such files are refused even on platforms that do not support + symbolic links. - Clear high mode bits (setuid, setgid, sticky) and group/other write bits (:const:`~stat.S_IWGRP` | :const:`~stat.S_IWOTH`). diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 5bc1aac8bec2afb..28d27577fe2961d 100755 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -819,6 +819,12 @@ def _get_filtered_attrs(member, dest_path, for_data=True): strict=os.path.ALLOW_MISSING) if os.path.commonpath([target_path, dest_path]) != dest_path: raise OutsideDestinationError(member, target_path) + # A link member named after the destination directory itself + # would replace it with a (sym)link, redirecting the destination + # for all subsequent members. + if ((member.islnk() or member.issym()) + and os.path.normpath(name) == os.curdir): + raise OutsideDestinationError(member, target_path) # Limit permissions (no high bits, and go-w) mode = member.mode if mode is not None: @@ -854,11 +860,6 @@ def _get_filtered_attrs(member, dest_path, for_data=True): if member.islnk() or member.issym(): if os.path.isabs(member.linkname): raise AbsoluteLinkError(member) - # A link member that resolves to the destination directory itself - # would replace it with a (sym)link, redirecting the destination - # for all subsequent members. - if target_path == dest_path: - raise OutsideDestinationError(member, target_path) normalized = os.path.normpath(member.linkname) if normalized != member.linkname: new_attrs['linkname'] = normalized diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 726ecff80124f8e..e4b93ddcde7244a 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -4304,8 +4304,10 @@ def test_link_at_destination(self): t.linkname = '.' arc.tar_w.addfile(t) - with self.check_context(arc.open(), 'data'): - self.expect_exception(tarfile.OutsideDestinationError) + for filter in 'tar', 'data': + with self.subTest(name=name, filter=filter): + with self.check_context(arc.open(), filter): + self.expect_exception(tarfile.OutsideDestinationError) @symlink_test @os_helper.skip_unless_symlink diff --git a/Misc/NEWS.d/next/Security/2026-10-21-19-58-53.gh-issue-160097.-opfAL.rst b/Misc/NEWS.d/next/Security/2026-10-21-19-58-53.gh-issue-160097.-opfAL.rst new file mode 100644 index 000000000000000..b48c8f7213ee826 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-10-21-19-58-53.gh-issue-160097.-opfAL.rst @@ -0,0 +1,5 @@ +.. cve: CVE-2026-114583 + +:func:`tarfile.tar_filter` now rejects link members whose path resolves to +the destination directory itself, as :func:`tarfile.data_filter` already +did.