From e7a447a84080ade0a69ca59df63e971697f198a4 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Thu, 22 Oct 2026 08:05:36 +0000 Subject: [PATCH] gh-160297: Fix tarfile setting attributes through a hard link fallback When a hard link member is extracted as a copy of a symlink member, do not apply the hard link's owner, mode and mtime to the symlink's target. ANT-2026-Y0H1Y0ER --- Lib/tarfile.py | 7 +++++ Lib/test/test_tarfile.py | 27 +++++++++++++++++++ ...-10-22-08-04-26.gh-issue-160297.hL4kQz.rst | 4 +++ 3 files changed, 38 insertions(+) create mode 100644 Misc/NEWS.d/next/Security/2026-10-22-08-04-26.gh-issue-160297.hL4kQz.rst diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 41394171832cbc..093367fb844f00 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -2733,6 +2733,13 @@ def _extract_member(self, tarinfo, targetpath, set_attrs=True, else: self.makefile(tarinfo, targetpath) + if tarinfo.islnk() and os.path.islink(targetpath): + # makelink_with_filter() fell back to extracting a copy of a + # symlink member, which already has its own attributes set. + # Setting the hard link's attributes here would follow the + # symlink, possibly to a file outside the destination directory. + set_attrs = False + if set_attrs: self.chown(tarinfo, targetpath, numeric_owner) if not tarinfo.issym(): diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index dba51d7131aa35..5eb1fc210a962e 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -4825,6 +4825,33 @@ def test_sneaky_hardlink_fallback_deep(self): self.expect_file("a/b/s", symlink_to=os.path.join('..', 'escape')) self.expect_file("s", symlink_to=os.path.join('..', 'escape')) + @symlink_test + def test_sneaky_hardlink_fallback_attrs(self): + # Test that when a hardlink is extracted as a copy of a symlink + # member, the hardlink's attributes are not applied to the file + # the symlink points to. + with ArchiveMaker() as arc: + arc.add("a/s", symlink_to=os.path.join("..", "flag")) + arc.add("s", hardlink_to=os.path.join("a", "s"), + mode='-rwxr-xr-x', mtime=86400, uid=1337, gid=1337) + + for filter in 'tar', 'fully_trusted': + with ( + self.subTest(filter), + unittest.mock.patch("os.chown", create=True) as mock_chown, + self.check_context(arc.open(), filter) as cc, + ): + if not os_helper.can_symlink(): + self.expect_file("a/") + else: + self.expect_file("a/s", symlink_to=os.path.join('..', 'flag')) + self.expect_file("s", symlink_to=os.path.join('..', 'flag')) + st = (cc.outerdir / 'flag').stat() + self.assertNotEqual(st.st_mtime, 86400) + if sys.platform != "win32": + self.assertNotEqual(st.st_mode & 0o777, 0o755) + mock_chown.assert_not_called() + @symlink_test @os_helper.skip_unless_hardlink def test_sneaky_hardlink_relocation(self): diff --git a/Misc/NEWS.d/next/Security/2026-10-22-08-04-26.gh-issue-160297.hL4kQz.rst b/Misc/NEWS.d/next/Security/2026-10-22-08-04-26.gh-issue-160297.hL4kQz.rst new file mode 100644 index 00000000000000..1419910948fe15 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-10-22-08-04-26.gh-issue-160297.hL4kQz.rst @@ -0,0 +1,4 @@ +Fix :meth:`tarfile.TarFile.extractall` and :meth:`tarfile.TarFile.extract` +changing the mode, modification time and owner of a file outside the +destination directory when a hard link member is extracted as a copy of a +symbolic link member, for example with the ``'tar'`` filter.