Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit c1a9c23

Browse files
authored
gh-57911: Sanitize symlink targets in tarfile on win32 (GH-138309)
1 parent e76464d commit c1a9c23

File tree

4 files changed

+71
-42
lines changed

4 files changed

+71
-42
lines changed

‎Doc/whatsnew/3.15.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,10 @@ tarfile
463463
:func:`~tarfile.TarFile.errorlevel` is zero.
464464
(Contributed by Matt Prodani and Petr Viktorin in :gh:`112887`
465465
and :cve:`2025-4435`.)
466+
* :func:`~tarfile.TarFile.extract` and :func:`~tarfile.TarFile.extractall`
467+
now replace slashes by backslashes in symlink targets on Windows to prevent
468+
creation of corrupted links.
469+
(Contributed by Christoph Walcher in :gh:`57911`.)
466470

467471

468472
types

‎Lib/tarfile.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2718,7 +2718,13 @@ def makelink_with_filter(self, tarinfo, targetpath,
27182718
if os.path.lexists(targetpath):
27192719
# Avoid FileExistsError on following os.symlink.
27202720
os.unlink(targetpath)
2721-
os.symlink(tarinfo.linkname, targetpath)
2721+
link_target = tarinfo.linkname
2722+
if os.name == "nt":
2723+
# gh-57911: Posix-flavoured forward-slash path separators in
2724+
# symlink targets aren't acknowledged by Windows, resulting
2725+
# in corrupted links.
2726+
link_target = link_target.replace("/", os.path.sep)
2727+
os.symlink(link_target, targetpath)
27222728
return
27232729
else:
27242730
if os.path.exists(tarinfo._link_target):

‎Lib/test/test_tarfile.py‎

Lines changed: 58 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2777,7 +2777,7 @@ def test_useful_error_message_when_modules_missing(self):
27772777
str(excinfo.exception),
27782778
)
27792779

2780-
@unittest.skipUnless(os_helper.can_symlink(), 'requires symlink support')
2780+
@os_helper.skip_unless_symlink
27812781
@unittest.skipUnless(hasattr(os, 'chmod'), "missing os.chmod")
27822782
@unittest.mock.patch('os.chmod')
27832783
def test_deferred_directory_attributes_update(self, mock_chmod):
@@ -3663,6 +3663,39 @@ class TestExtractionFilters(unittest.TestCase):
36633663
# The destination for the extraction, within `outerdir`
36643664
destdir = outerdir / 'dest'
36653665

3666+
@classmethod
3667+
def setUpClass(cls):
3668+
# Posix and Windows have different pathname resolution:
3669+
# either symlink or a '..' component resolve first.
3670+
# Let's see which we are on.
3671+
if os_helper.can_symlink():
3672+
testpath = os.path.join(TEMPDIR, 'resolution_test')
3673+
os.mkdir(testpath)
3674+
3675+
# testpath/current links to `.` which is all of:
3676+
# - `testpath`
3677+
# - `testpath/current`
3678+
# - `testpath/current/current`
3679+
# - etc.
3680+
os.symlink('.', os.path.join(testpath, 'current'))
3681+
3682+
# we'll test where `testpath/current/../file` ends up
3683+
with open(os.path.join(testpath, 'current', '..', 'file'), 'w'):
3684+
pass
3685+
3686+
if os.path.exists(os.path.join(testpath, 'file')):
3687+
# Windows collapses 'current\..' to '.' first, leaving
3688+
# 'testpath\file'
3689+
cls.dotdot_resolves_early = True
3690+
elif os.path.exists(os.path.join(testpath, '..', 'file')):
3691+
# Posix resolves 'current' to '.' first, leaving
3692+
# 'testpath/../file'
3693+
cls.dotdot_resolves_early = False
3694+
else:
3695+
raise AssertionError('Could not determine link resolution')
3696+
else:
3697+
cls.dotdot_resolves_early = True
3698+
36663699
@contextmanager
36673700
def check_context(self, tar, filter, *, check_flag=True):
36683701
"""Extracts `tar` to `self.destdir` and allows checking the result
@@ -3809,23 +3842,21 @@ def test_parent_symlink(self):
38093842
arc.add('current', symlink_to='.')
38103843

38113844
# effectively points to ./../
3812-
arc.add('parent', symlink_to='current/..')
3845+
if self.dotdot_resolves_early:
3846+
arc.add('parent', symlink_to='current/../..')
3847+
else:
3848+
arc.add('parent', symlink_to='current/..')
38133849

38143850
arc.add('parent/evil')
38153851

38163852
if os_helper.can_symlink():
38173853
with self.check_context(arc.open(), 'fully_trusted'):
3818-
if self.raised_exception is not None:
3819-
# Windows will refuse to create a file that's a symlink to itself
3820-
# (and tarfile doesn't swallow that exception)
3821-
self.expect_exception(FileExistsError)
3822-
# The other cases will fail with this error too.
3823-
# Skip the rest of this test.
3824-
return
3854+
self.expect_file('current', symlink_to='.')
3855+
if self.dotdot_resolves_early:
3856+
self.expect_file('parent', symlink_to='current/../..')
38253857
else:
3826-
self.expect_file('current', symlink_to='.')
38273858
self.expect_file('parent', symlink_to='current/..')
3828-
self.expect_file('../evil')
3859+
self.expect_file('../evil')
38293860

38303861
with self.check_context(arc.open(), 'tar'):
38313862
self.expect_exception(
@@ -3927,35 +3958,6 @@ def test_parent_symlink2(self):
39273958
# Test interplaying symlinks
39283959
# Inspired by 'dirsymlink2b' in jwilk/traversal-archives
39293960

3930-
# Posix and Windows have different pathname resolution:
3931-
# either symlink or a '..' component resolve first.
3932-
# Let's see which we are on.
3933-
if os_helper.can_symlink():
3934-
testpath = os.path.join(TEMPDIR, 'resolution_test')
3935-
os.mkdir(testpath)
3936-
3937-
# testpath/current links to `.` which is all of:
3938-
# - `testpath`
3939-
# - `testpath/current`
3940-
# - `testpath/current/current`
3941-
# - etc.
3942-
os.symlink('.', os.path.join(testpath, 'current'))
3943-
3944-
# we'll test where `testpath/current/../file` ends up
3945-
with open(os.path.join(testpath, 'current', '..', 'file'), 'w'):
3946-
pass
3947-
3948-
if os.path.exists(os.path.join(testpath, 'file')):
3949-
# Windows collapses 'current\..' to '.' first, leaving
3950-
# 'testpath\file'
3951-
dotdot_resolves_early = True
3952-
elif os.path.exists(os.path.join(testpath, '..', 'file')):
3953-
# Posix resolves 'current' to '.' first, leaving
3954-
# 'testpath/../file'
3955-
dotdot_resolves_early = False
3956-
else:
3957-
raise AssertionError('Could not determine link resolution')
3958-
39593961
with ArchiveMaker() as arc:
39603962

39613963
# `current` links to `.` which is both the destination directory
@@ -3991,7 +3993,7 @@ def test_parent_symlink2(self):
39913993

39923994
with self.check_context(arc.open(), 'data'):
39933995
if os_helper.can_symlink():
3994-
if dotdot_resolves_early:
3996+
if self.dotdot_resolves_early:
39953997
# Fail when extracting a file outside destination
39963998
self.expect_exception(
39973999
tarfile.OutsideDestinationError,
@@ -4039,6 +4041,21 @@ def test_absolute_symlink(self):
40394041
tarfile.AbsoluteLinkError,
40404042
"'parent' is a link to an absolute path")
40414043

4044+
@symlink_test
4045+
@os_helper.skip_unless_symlink
4046+
def test_symlink_target_seperator_rewrite_on_windows(self):
4047+
with ArchiveMaker() as arc:
4048+
arc.add('link', symlink_to="relative/test/path")
4049+
4050+
with self.check_context(arc.open(), 'fully_trusted'):
4051+
self.expect_file('link', type=tarfile.SYMTYPE)
4052+
link_path = os.path.normpath(self.destdir / "link")
4053+
link_target = os.readlink(link_path)
4054+
if os.name == "nt":
4055+
self.assertEqual(link_target, "relative\\test\\path")
4056+
else:
4057+
self.assertEqual(link_target, "relative/test/path")
4058+
40424059
def test_absolute_hardlink(self):
40434060
# Test hardlink to an absolute path
40444061
# Inspired by 'dirsymlink' in https://github.com/jwilk/traversal-archives
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
When extracting tar files on Windows, slashes in symlink targets will be
2+
replaced by backslashes to prevent corrupted links.

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /