From 78b65b9d5a36e63becb3394b104c5508aba9eefa Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 22 Oct 2026 21:23:08 +0100 Subject: [PATCH] gh-160097: Reject links replacing the destination in `tarfile.tar_filter` (GH-160098) (cherry picked from commit e7e0b3523ad6ba8472873dda594cf4021a16f8b5) Co-authored-by: Stan Ulbrych --- Doc/library/tarfile.rst | 5 +++++ Lib/tarfile.py | 11 ++++++----- Lib/test/test_tarfile.py | 6 ++++-- .../2026-10-21-19-58-53.gh-issue-160097.-opfAL.rst | 5 +++++ 4 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2026-10-21-19-58-53.gh-issue-160097.-opfAL.rst diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst index e8c272cb39716b..27bf353fe76bca 100644 --- a/Doc/library/tarfile.rst +++ b/Doc/library/tarfile.rst @@ -1040,6 +1040,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 8c2dd869db4788..a35773017e5cb7 100755 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -789,6 +789,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: @@ -824,11 +830,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 c4009927b0aa3b..01747fcf6e16fb 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -3971,8 +3971,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 00000000000000..b48c8f7213ee82 --- /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.