From 0d328646583656a58e6c59d815e4dd1a18e69a5e Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 10 Sep 2026 11:06:35 +0100 Subject: [PATCH] [3.12] gh-157190: Fix tarfile `data`/`tar` filter bypass via hard link to a symlink (GH-157191) (GH-157192) (cherry picked from commit 480ea4aab898291132c071645ec9ef26dab69def) The backport to 3.13 and below includes a NEWS entry. (cherry picked from commit b8f23e307097552eaea2604383a12ab280520d0d) Co-authored-by: Stan Ulbrych --- Lib/tarfile.py | 6 +++++- Lib/test/test_tarfile.py | 18 ++++++++++++++++++ ...6-09-06-11-02-46.gh-issue-157190.tarhln.rst | 5 +++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Security/2026-09-06-11-02-46.gh-issue-157190.tarhln.rst diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 13283e52b20bf55..86d47f30d99ff11 100755 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -2665,7 +2665,11 @@ def makelink_with_filter(self, tarinfo, targetpath, return else: if os.path.exists(tarinfo._link_target): - os.link(tarinfo._link_target, targetpath) + # Resolve the target so the hard link points to the file + # itself. Otherwise os.link() may duplicate a symlink to a + # shallower location, where it's relative target escapes the + # destination directory. (CVE-2026-82049) + os.link(os.path.realpath(tarinfo._link_target), targetpath) return except symlink_exception: keyerror_to_extracterror = True diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index cf2c3c66561f9d7..b869fd6fa15e4c4 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -4220,6 +4220,24 @@ 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 + @os_helper.skip_unless_hardlink + def test_sneaky_hardlink_relocation(self): + with ArchiveMaker() as arc: + arc.add("a/escape", content="decoy") + arc.add("a/b/s", symlink_to=os.path.join("..", "escape")) + arc.add("s", hardlink_to=os.path.join("a", "b", "s")) + + for filter in 'data', 'tar': + with self.subTest(filter), self.check_context(arc.open(), filter): + self.expect_file("a/escape", content="decoy") + if os_helper.can_symlink(): + self.expect_file("a/b/s", symlink_to=os.path.join('..', 'escape')) + else: + self.expect_file("a/b/s", content="decoy") + self.expect_file("s", content="decoy") + self.assertFalse((self.destdir / "s").is_symlink()) + @symlink_test def test_exfiltration_via_symlink(self): # (CVE-2025-4138) diff --git a/Misc/NEWS.d/next/Security/2026-09-06-11-02-46.gh-issue-157190.tarhln.rst b/Misc/NEWS.d/next/Security/2026-09-06-11-02-46.gh-issue-157190.tarhln.rst new file mode 100644 index 000000000000000..3dffa3bf5b5a5a8 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-09-06-11-02-46.gh-issue-157190.tarhln.rst @@ -0,0 +1,5 @@ +Fixed a vulnerability in the :mod:`tarfile` ``data`` and ``tar`` extraction +filters where a crafted archive using a hard link to a symbolic link could +change the permissions and modification time of a file outside the +destination directory, and expose its contents inside the extracted tree. +This addresses :cve:`2026-82049`.