From 8947727186fff1897329325d690af832a5d38e4a Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <stan@python.org>
Date: Tue, 23 Jun 2026 14:31:38 +0100
Subject: [PATCH 1/2] gh-151558: Fix symlink escape via `tarfile`
 hardlink-extraction fallback (GH-151559) (cherry picked from commit
 27dd970bf6b17ebca7c8ed486a40ab043ed7af8f)

Co-authored-by: Stan Ulbrych <stan@python.org>
---
 Lib/tarfile.py                                                           |    3 +
 Lib/test/test_tarfile.py                                                 |   24 ++++++++++
 Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst |    3 +
 3 files changed, 30 insertions(+)
 create mode 100644 Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst

Index: Python-3.11.15/Lib/tarfile.py
===================================================================
--- Python-3.11.15.orig/Lib/tarfile.py	2026-07-02 12:09:33.732145754 +0200
+++ Python-3.11.15/Lib/tarfile.py	2026-07-02 12:09:33.864108270 +0200
@@ -2662,6 +2662,9 @@
                     "makelink_with_filter: if filter_function is not None, "
                     + "extraction_root must also not be None")
             try:
+                filter_function(
+                    unfiltered.replace(name=tarinfo.name, deep=False),
+                    extraction_root)
                 filtered = filter_function(unfiltered, extraction_root)
             except _FILTER_ERRORS as cause:
                 raise LinkFallbackError(tarinfo, unfiltered.name) from cause
Index: Python-3.11.15/Lib/test/test_tarfile.py
===================================================================
--- Python-3.11.15.orig/Lib/test/test_tarfile.py	2026-07-02 12:09:33.733105801 +0200
+++ Python-3.11.15/Lib/test/test_tarfile.py	2026-07-02 12:09:33.864793876 +0200
@@ -3999,6 +3999,30 @@
                     self.expect_file("c", symlink_to='b')
 
     @symlink_test
+    def test_sneaky_hardlink_fallback_deep(self):
+        # (CVE-2026-11940)
+        with ArchiveMaker() as arc:
+            arc.add("a/b/s", symlink_to=os.path.join("..", "escape"))
+            arc.add("s", hardlink_to=os.path.join("a", "b", "s"))
+
+        with self.check_context(arc.open(), 'data'):
+            e = self.expect_exception(
+                tarfile.LinkFallbackError,
+                "link 's' would be extracted as a copy of "
+                + "'a/b/s', which was rejected")
+            self.assertIsInstance(e.__cause__,
+                                  tarfile.LinkOutsideDestinationError)
+
+        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/")
+                    self.expect_file("a/b/")
+                else:
+                    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_exfiltration_via_symlink(self):
         # (CVE-2025-4138)
         # Test changing symlinks that result in a symlink pointing outside
Index: Python-3.11.15/Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ Python-3.11.15/Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst	2026-07-02 12:09:33.865970015 +0200
@@ -0,0 +1,3 @@
+Fixed an vulnerability in the :mod:`tarfile` ``data`` and ``tar`` extraction
+filters where crafted archives could create a symlink pointing outside the
+destination directory. This was a bypass of CVE-2025-4330.
