| From e24b4e95524fbe8cd0f46aa3292e8040f0e07c83 Mon Sep 17 00:00:00 2001 |
| From: "Miss Islington (bot)" |
| <31488909+miss-islington@users.noreply.github.com> |
| Date: Tue, 23 Jun 2026 15:58:47 +0200 |
| Subject: [PATCH 1/2] gh-151558: Fix symlink escape via `tarfile` |
| hardlink-extraction fallback (GH-151559) |
| |
| CVE: CVE-2026-11940 |
| Upstream-Status: Backport [https://github.com/python/cpython/commit/27dd970bf6b17ebca7c8ed486a40ab043ed7af8f] |
| |
| Signed-off-by: Benjamin Robin <benjamin.robin@bootlin.com> |
| --- |
| Lib/tarfile.py | 3 +++ |
| Lib/test/test_tarfile.py | 24 ++++++++++++++++++++++++ |
| 2 files changed, 27 insertions(+) |
| |
| diff --git a/Lib/tarfile.py b/Lib/tarfile.py |
| index e6734db24f64..63f23490e8a1 100644 |
| --- a/Lib/tarfile.py |
| +++ b/Lib/tarfile.py |
| @@ -2782,6 +2782,9 @@ 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( |
| + 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 |
| diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py |
| index d974c7d46ec1..0fc7413be8db 100644 |
| --- a/Lib/test/test_tarfile.py |
| +++ b/Lib/test/test_tarfile.py |
| @@ -4344,6 +4344,30 @@ def test_sneaky_hardlink_fallback(self): |
| self.expect_file("boom", symlink_to='../../link_here') |
| 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) |
| -- |
| 2.54.0 |