From 56b8673babe5936d41ae6affdfed258b6805a443 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Mon, 19 Oct 2026 20:53:08 +0100 Subject: [PATCH 1/3] Reject links replacing the destination in `tarfile.tar_filter` --- Doc/library/tarfile.rst | 5 +++++ Lib/tarfile.py | 10 +++++----- Lib/test/test_tarfile.py | 6 ++++-- .../2026-10-19-20-50-41.gh-issue-XXXXXX.dhsJf6.rst | 2 ++ 4 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2026-10-19-20-50-41.gh-issue-XXXXXX.dhsJf6.rst diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst index 1b2c328f9cad9f6..08ff5b63aafd50b 100644 --- a/Doc/library/tarfile.rst +++ b/Doc/library/tarfile.rst @@ -1131,6 +1131,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 path resolves 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 c0b9f3f429c10e2..f76884ecbe93b14 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -848,6 +848,11 @@ 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 that resolves to 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 target_path == dest_path: + raise OutsideDestinationError(member, target_path) # Limit permissions (no high bits, and go-w) mode = member.mode if mode is not None: @@ -883,11 +888,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 2b4cf748dffc1a9..067430ac89d4b3f 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -4545,8 +4545,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-19-20-50-41.gh-issue-XXXXXX.dhsJf6.rst b/Misc/NEWS.d/next/Security/2026-10-19-20-50-41.gh-issue-XXXXXX.dhsJf6.rst new file mode 100644 index 000000000000000..c05084a4a3f1067 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-10-19-20-50-41.gh-issue-XXXXXX.dhsJf6.rst @@ -0,0 +1,2 @@ +:func:`tarfile.tar_filter` now rejects link members whose path resolves to +the destination directory itself, as :func:`tarfile.data_filter` already did. From 607f0abaca1341ca57566ea381ab98a372530cd3 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Wed, 21 Oct 2026 19:59:14 +0100 Subject: [PATCH 2/3] Update news entry! --- ...f6.rst => 2026-10-21-19-58-53.gh-issue-160097.-opfAL.rst} | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) rename Misc/NEWS.d/next/Security/{2026-10-19-20-50-41.gh-issue-XXXXXX.dhsJf6.rst => 2026-10-21-19-58-53.gh-issue-160097.-opfAL.rst} (78%) diff --git a/Misc/NEWS.d/next/Security/2026-10-19-20-50-41.gh-issue-XXXXXX.dhsJf6.rst b/Misc/NEWS.d/next/Security/2026-10-21-19-58-53.gh-issue-160097.-opfAL.rst similarity index 78% rename from Misc/NEWS.d/next/Security/2026-10-19-20-50-41.gh-issue-XXXXXX.dhsJf6.rst rename to Misc/NEWS.d/next/Security/2026-10-21-19-58-53.gh-issue-160097.-opfAL.rst index c05084a4a3f1067..b48c8f7213ee826 100644 --- a/Misc/NEWS.d/next/Security/2026-10-19-20-50-41.gh-issue-XXXXXX.dhsJf6.rst +++ b/Misc/NEWS.d/next/Security/2026-10-21-19-58-53.gh-issue-160097.-opfAL.rst @@ -1,2 +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. +the destination directory itself, as :func:`tarfile.data_filter` already +did. From 409006369d5dad5cee80a272a17c14594ed05067 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Wed, 21 Oct 2026 20:28:14 +0100 Subject: [PATCH 3/3] Make the destination link check lexical --- Doc/library/tarfile.rst | 2 +- Lib/tarfile.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst index 08ff5b63aafd50b..06eba01d21e5261 100644 --- a/Doc/library/tarfile.rst +++ b/Doc/library/tarfile.rst @@ -1132,7 +1132,7 @@ reused in custom filters: path (after following symlinks) would end up outside the destination. This raises :class:`~tarfile.OutsideDestinationError`. - :ref:`Refuse ` to extract links (hard or soft) - whose path resolves to the destination directory itself. + 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. diff --git a/Lib/tarfile.py b/Lib/tarfile.py index f76884ecbe93b14..d4c4af92aa362ea 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -848,10 +848,11 @@ 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 that resolves to the destination directory itself + # 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 target_path == dest_path: + 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