From 480ea4aab898291132c071645ec9ef26dab69def Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sun, 6 Sep 2026 12:51:37 +0100 Subject: [PATCH 1/3] Fix symlink escape via `tarfile` hard link to symlink --- Lib/tarfile.py | 6 +++++- Lib/test/test_tarfile.py | 17 +++++++++++++++++ ...26-09-06-11-02-46.gh-issue-XXXXXX.tarhln.rst | 5 +++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Security/2026-09-06-11-02-46.gh-issue-XXXXXX.tarhln.rst diff --git a/Lib/tarfile.py b/Lib/tarfile.py index f46e938fd314ddb..7a59c7f261f6b73 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -2813,7 +2813,11 @@ def makelink_with_filter(self, tarinfo, targetpath, if os.path.lexists(targetpath): # Avoid FileExistsError on following os.link. os.unlink(targetpath) - 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 10106c3ada9ba52..81c6732d7fe37e9 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -4642,6 +4642,23 @@ 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_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-XXXXXX.tarhln.rst b/Misc/NEWS.d/next/Security/2026-09-06-11-02-46.gh-issue-XXXXXX.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-XXXXXX.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`. From ac398e33890bf42d7f5671f7c81868c68737c4bb Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 8 Sep 2026 16:20:11 +0100 Subject: [PATCH 2/3] Drop news entry for this branch. --- .../Security/2026-09-06-11-02-46.gh-issue-XXXXXX.tarhln.rst | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 Misc/NEWS.d/next/Security/2026-09-06-11-02-46.gh-issue-XXXXXX.tarhln.rst diff --git a/Misc/NEWS.d/next/Security/2026-09-06-11-02-46.gh-issue-XXXXXX.tarhln.rst b/Misc/NEWS.d/next/Security/2026-09-06-11-02-46.gh-issue-XXXXXX.tarhln.rst deleted file mode 100644 index 3dffa3bf5b5a5a8..000000000000000 --- a/Misc/NEWS.d/next/Security/2026-09-06-11-02-46.gh-issue-XXXXXX.tarhln.rst +++ /dev/null @@ -1,5 +0,0 @@ -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`. From 027cfba95f551cbfa586d2c903dfc2e2071c4351 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 8 Sep 2026 16:59:51 +0100 Subject: [PATCH 3/3] `skip_unless_hardlink` for Android/Emscriptem. --- Lib/test/test_tarfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 81c6732d7fe37e9..688e17c1e872f92 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -4643,6 +4643,7 @@ def test_sneaky_hardlink_fallback_deep(self): 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")