From 3d66a90357160e7122f318c657076fabeca982fa Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 26 Aug 2026 11:29:07 +0200 Subject: [PATCH 1/3] Honor None result of filter for link fallbacks --- Lib/tarfile.py | 4 +++- Lib/test/test_tarfile.py | 32 +++++++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 6e092f1dcee5e88..a4f9ce3311f6dad 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -2841,9 +2841,11 @@ def makelink_with_filter(self, tarinfo, targetpath, "makelink_with_filter: if filter_function is not None, " + "extraction_root must also not be None") try: - filter_function( + filtered = filter_function( unfiltered.replace(name=tarinfo.name, deep=False), extraction_root) + if filtered is None: + return filtered = filter_function(unfiltered, extraction_root) except _FILTER_ERRORS as cause: raise LinkFallbackError(tarinfo, unfiltered.name) from cause diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index be5abfe211fb929..61201ee0b8c7617 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -4621,9 +4621,15 @@ def test_sneaky_hardlink_fallback(self): for filter in 'tar', 'fully_trusted': with self.subTest(filter), self.check_context(arc.open(), filter): if not os_helper.can_symlink(): - self.expect_file("a/t/dummy") - self.expect_file("b/") - self.expect_file("c/") + if filter == 'tar': + self.expect_exception( + tarfile.LinkFallbackError, + "link 'boom' would be extracted as a copy of " + + "'c/escape', which was rejected") + else: + self.expect_file("a/t/dummy") + self.expect_file("b/") + self.expect_file("c/") else: self.expect_file("a/t/dummy") self.expect_file("b/") @@ -4820,6 +4826,26 @@ def testing_filter(member, path): if os_helper.can_chmod(): self.assertFalse(path.stat().st_mode & stat.S_IWUSR) + @symlink_test + def test_extract_filters_target_none(self): + # Test that when extract() falls back to extracting (rather than + # linking) a hardlink target, the member is rejected if the filter + # returns None. + with ArchiveMaker() as arc: + arc.add('a/b/s', symlink_to='../escape') + arc.add('q', hardlink_to='a/b/s') + def filter_unsafe_members(member, path): + try: + return tarfile.data_filter(member, path) + except tarfile.FilterError as error: + return None + tempdir = pathlib.Path(TEMPDIR) / 'extract' + with self.check_context(arc.open(), filter_unsafe_members): + if os_helper.can_symlink(): + self.expect_file('a/b/s', symlink_to='../escape') + else: + self.expect_file('a/b/') # symlink is not extracted + def test_link_fallback_normalizes(self): # Make sure hardlink fallbacks work for non-normalized paths for all # filters From 5c3e14662b21f7b9b30f435ee2922eb905b24ee0 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 10 Sep 2026 13:12:40 +0200 Subject: [PATCH 2/3] Apply review suggestions Co-authored-by: Stan Ulbrych --- Lib/test/test_tarfile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 61201ee0b8c7617..598179983a7cbc5 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -4829,7 +4829,7 @@ def testing_filter(member, path): @symlink_test def test_extract_filters_target_none(self): # Test that when extract() falls back to extracting (rather than - # linking) a hardlink target, the member is rejected if the filter + # linking) a hardlink target, the member is skipped if the filter # returns None. with ArchiveMaker() as arc: arc.add('a/b/s', symlink_to='../escape') @@ -4839,7 +4839,6 @@ def filter_unsafe_members(member, path): return tarfile.data_filter(member, path) except tarfile.FilterError as error: return None - tempdir = pathlib.Path(TEMPDIR) / 'extract' with self.check_context(arc.open(), filter_unsafe_members): if os_helper.can_symlink(): self.expect_file('a/b/s', symlink_to='../escape') From aebd409a795adbee5b7daaf71d5af57cc1b7fd1f Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 10 Sep 2026 13:38:41 +0200 Subject: [PATCH 3/3] Add NEWS file --- .../Security/2026-09-10-13-38-11.gh-issue-157265.-vYuMp.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Security/2026-09-10-13-38-11.gh-issue-157265.-vYuMp.rst diff --git a/Misc/NEWS.d/next/Security/2026-09-10-13-38-11.gh-issue-157265.-vYuMp.rst b/Misc/NEWS.d/next/Security/2026-09-10-13-38-11.gh-issue-157265.-vYuMp.rst new file mode 100644 index 000000000000000..ba27e47f734bfb1 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-09-10-13-38-11.gh-issue-157265.-vYuMp.rst @@ -0,0 +1,3 @@ +In :mod:`tarfile`, when extracting a link falls back to extracting a member +of the archive, skip the member when the filter function returns None when +called with the extracted member's name replaced with the link's.