Skip to content

Commit 197663d

Browse files
[3.10] gh-157190: Fix tarfile data/tar filter bypass via hard link to a symlink (GH-157191) (#158457)
The backport to 3.13 and below includes a NEWS entry. (cherry picked from commit b8f23e3) Co-authored-by: Stan Ulbrych <stan@python.org>
1 parent 42dd786 commit 197663d

4 files changed

Lines changed: 47 additions & 1 deletion

File tree

‎Lib/tarfile.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2648,7 +2648,11 @@ def makelink_with_filter(self, tarinfo, targetpath,
26482648
return
26492649
else:
26502650
if os.path.exists(tarinfo._link_target):
2651-
os.link(tarinfo._link_target, targetpath)
2651+
# Resolve the target so the hard link points to the file
2652+
# itself. Otherwise os.link() may duplicate a symlink to a
2653+
# shallower location, where it's relative target escapes the
2654+
# destination directory. (CVE-2026-82049)
2655+
os.link(os.path.realpath(tarinfo._link_target), targetpath)
26522656
return
26532657
except symlink_exception:
26542658
keyerror_to_extracterror = True

‎Lib/test/support/os_helper.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import unittest
1010
import warnings
1111

12+
from test import support
13+
1214

1315
# Filename used for testing
1416
if os.name == 'java':
@@ -190,6 +192,24 @@ def skip_unless_symlink(test):
190192
return test if ok else unittest.skip(msg)(test)
191193

192194

195+
_can_hardlink = None
196+
197+
198+
def can_hardlink():
199+
global _can_hardlink
200+
if _can_hardlink is None:
201+
# Android blocks hard links using SELinux
202+
# (https://stackoverflow.com/q/32365690).
203+
_can_hardlink = hasattr(os, "link") and not support.is_android
204+
return _can_hardlink
205+
206+
207+
def skip_unless_hardlink(test):
208+
ok = can_hardlink()
209+
msg = "requires hardlink support"
210+
return test if ok else unittest.skip(msg)(test)
211+
212+
193213
_can_xattr = None
194214

195215

‎Lib/test/test_tarfile.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4010,6 +4010,23 @@ def test_sneaky_hardlink_fallback_deep(self):
40104010
self.expect_file("a/b/s", symlink_to=os.path.join('..', 'escape'))
40114011
self.expect_file("s", symlink_to=os.path.join('..', 'escape'))
40124012

4013+
@os_helper.skip_unless_hardlink
4014+
def test_sneaky_hardlink_relocation(self):
4015+
with ArchiveMaker() as arc:
4016+
arc.add("a/escape", content="decoy")
4017+
arc.add("a/b/s", symlink_to=os.path.join("..", "escape"))
4018+
arc.add("s", hardlink_to=os.path.join("a", "b", "s"))
4019+
4020+
for filter in 'data', 'tar':
4021+
with self.subTest(filter), self.check_context(arc.open(), filter):
4022+
self.expect_file("a/escape", content="decoy")
4023+
if os_helper.can_symlink():
4024+
self.expect_file("a/b/s", symlink_to=os.path.join('..', 'escape'))
4025+
else:
4026+
self.expect_file("a/b/s", content="decoy")
4027+
self.expect_file("s", content="decoy")
4028+
self.assertFalse((self.destdir / "s").is_symlink())
4029+
40134030
def test_exfiltration_via_symlink(self):
40144031
# (CVE-2025-4138)
40154032
# Test changing symlinks that result in a symlink pointing outside
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fixed a vulnerability in the :mod:`tarfile` ``data`` and ``tar`` extraction
2+
filters where a crafted archive using a hard link to a symbolic link could
3+
change the permissions and modification time of a file outside the
4+
destination directory, and expose its contents inside the extracted tree.
5+
This addresses CVE 2026-82049.

0 commit comments

Comments
 (0)