mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +00:00
[3.11] gh-107845: Fix symlink handling for tarfile.data_filter (GH-107846) (GH-108209)
gh-107845: Fix symlink handling for tarfile.data_filter (GH-107846)
(cherry picked from commit acbd3f9c5c)
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Co-authored-by: Victor Stinner <vstinner@python.org>
Co-authored-by: Lumír 'Frenzy' Balhar <frenzy.madness@gmail.com>
This commit is contained in:
parent
dd0a1f9da2
commit
8e837373ed
4 changed files with 156 additions and 9 deletions
|
|
@ -732,6 +732,11 @@ A ``TarInfo`` object has the following public data attributes:
|
||||||
Name of the target file name, which is only present in :class:`TarInfo` objects
|
Name of the target file name, which is only present in :class:`TarInfo` objects
|
||||||
of type :const:`LNKTYPE` and :const:`SYMTYPE`.
|
of type :const:`LNKTYPE` and :const:`SYMTYPE`.
|
||||||
|
|
||||||
|
For symbolic links (``SYMTYPE``), the *linkname* is relative to the directory
|
||||||
|
that contains the link.
|
||||||
|
For hard links (``LNKTYPE``), the *linkname* is relative to the root of
|
||||||
|
the archive.
|
||||||
|
|
||||||
|
|
||||||
.. attribute:: TarInfo.uid
|
.. attribute:: TarInfo.uid
|
||||||
:type: int
|
:type: int
|
||||||
|
|
|
||||||
|
|
@ -741,7 +741,7 @@ def __init__(self, tarinfo):
|
||||||
class AbsoluteLinkError(FilterError):
|
class AbsoluteLinkError(FilterError):
|
||||||
def __init__(self, tarinfo):
|
def __init__(self, tarinfo):
|
||||||
self.tarinfo = tarinfo
|
self.tarinfo = tarinfo
|
||||||
super().__init__(f'{tarinfo.name!r} is a symlink to an absolute path')
|
super().__init__(f'{tarinfo.name!r} is a link to an absolute path')
|
||||||
|
|
||||||
class LinkOutsideDestinationError(FilterError):
|
class LinkOutsideDestinationError(FilterError):
|
||||||
def __init__(self, tarinfo, path):
|
def __init__(self, tarinfo, path):
|
||||||
|
|
@ -801,7 +801,14 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
|
||||||
if member.islnk() or member.issym():
|
if member.islnk() or member.issym():
|
||||||
if os.path.isabs(member.linkname):
|
if os.path.isabs(member.linkname):
|
||||||
raise AbsoluteLinkError(member)
|
raise AbsoluteLinkError(member)
|
||||||
target_path = os.path.realpath(os.path.join(dest_path, member.linkname))
|
if member.issym():
|
||||||
|
target_path = os.path.join(dest_path,
|
||||||
|
os.path.dirname(name),
|
||||||
|
member.linkname)
|
||||||
|
else:
|
||||||
|
target_path = os.path.join(dest_path,
|
||||||
|
member.linkname)
|
||||||
|
target_path = os.path.realpath(target_path)
|
||||||
if os.path.commonpath([target_path, dest_path]) != dest_path:
|
if os.path.commonpath([target_path, dest_path]) != dest_path:
|
||||||
raise LinkOutsideDestinationError(member, target_path)
|
raise LinkOutsideDestinationError(member, target_path)
|
||||||
return new_attrs
|
return new_attrs
|
||||||
|
|
|
||||||
|
|
@ -3256,10 +3256,12 @@ def __exit__(self, *exc):
|
||||||
self.bio = None
|
self.bio = None
|
||||||
|
|
||||||
def add(self, name, *, type=None, symlink_to=None, hardlink_to=None,
|
def add(self, name, *, type=None, symlink_to=None, hardlink_to=None,
|
||||||
mode=None, **kwargs):
|
mode=None, size=None, **kwargs):
|
||||||
"""Add a member to the test archive. Call within `with`."""
|
"""Add a member to the test archive. Call within `with`."""
|
||||||
name = str(name)
|
name = str(name)
|
||||||
tarinfo = tarfile.TarInfo(name).replace(**kwargs)
|
tarinfo = tarfile.TarInfo(name).replace(**kwargs)
|
||||||
|
if size is not None:
|
||||||
|
tarinfo.size = size
|
||||||
if mode:
|
if mode:
|
||||||
tarinfo.mode = _filemode_to_int(mode)
|
tarinfo.mode = _filemode_to_int(mode)
|
||||||
if symlink_to is not None:
|
if symlink_to is not None:
|
||||||
|
|
@ -3335,7 +3337,8 @@ def check_context(self, tar, filter):
|
||||||
raise self.raised_exception
|
raise self.raised_exception
|
||||||
self.assertEqual(self.expected_paths, set())
|
self.assertEqual(self.expected_paths, set())
|
||||||
|
|
||||||
def expect_file(self, name, type=None, symlink_to=None, mode=None):
|
def expect_file(self, name, type=None, symlink_to=None, mode=None,
|
||||||
|
size=None):
|
||||||
"""Check a single file. See check_context."""
|
"""Check a single file. See check_context."""
|
||||||
if self.raised_exception:
|
if self.raised_exception:
|
||||||
raise self.raised_exception
|
raise self.raised_exception
|
||||||
|
|
@ -3364,6 +3367,8 @@ def expect_file(self, name, type=None, symlink_to=None, mode=None):
|
||||||
self.assertTrue(path.is_fifo())
|
self.assertTrue(path.is_fifo())
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(type)
|
raise NotImplementedError(type)
|
||||||
|
if size is not None:
|
||||||
|
self.assertEqual(path.stat().st_size, size)
|
||||||
for parent in path.parents:
|
for parent in path.parents:
|
||||||
self.expected_paths.discard(parent)
|
self.expected_paths.discard(parent)
|
||||||
|
|
||||||
|
|
@ -3410,8 +3415,15 @@ def test_parent_symlink(self):
|
||||||
# Test interplaying symlinks
|
# Test interplaying symlinks
|
||||||
# Inspired by 'dirsymlink2a' in jwilk/traversal-archives
|
# Inspired by 'dirsymlink2a' in jwilk/traversal-archives
|
||||||
with ArchiveMaker() as arc:
|
with ArchiveMaker() as arc:
|
||||||
|
|
||||||
|
# `current` links to `.` which is both:
|
||||||
|
# - the destination directory
|
||||||
|
# - `current` itself
|
||||||
arc.add('current', symlink_to='.')
|
arc.add('current', symlink_to='.')
|
||||||
|
|
||||||
|
# effectively points to ./../
|
||||||
arc.add('parent', symlink_to='current/..')
|
arc.add('parent', symlink_to='current/..')
|
||||||
|
|
||||||
arc.add('parent/evil')
|
arc.add('parent/evil')
|
||||||
|
|
||||||
if os_helper.can_symlink():
|
if os_helper.can_symlink():
|
||||||
|
|
@ -3453,9 +3465,46 @@ def test_parent_symlink(self):
|
||||||
def test_parent_symlink2(self):
|
def test_parent_symlink2(self):
|
||||||
# Test interplaying symlinks
|
# Test interplaying symlinks
|
||||||
# Inspired by 'dirsymlink2b' in jwilk/traversal-archives
|
# Inspired by 'dirsymlink2b' in jwilk/traversal-archives
|
||||||
|
|
||||||
|
# Posix and Windows have different pathname resolution:
|
||||||
|
# either symlink or a '..' component resolve first.
|
||||||
|
# Let's see which we are on.
|
||||||
|
if os_helper.can_symlink():
|
||||||
|
testpath = os.path.join(TEMPDIR, 'resolution_test')
|
||||||
|
os.mkdir(testpath)
|
||||||
|
|
||||||
|
# testpath/current links to `.` which is all of:
|
||||||
|
# - `testpath`
|
||||||
|
# - `testpath/current`
|
||||||
|
# - `testpath/current/current`
|
||||||
|
# - etc.
|
||||||
|
os.symlink('.', os.path.join(testpath, 'current'))
|
||||||
|
|
||||||
|
# we'll test where `testpath/current/../file` ends up
|
||||||
|
with open(os.path.join(testpath, 'current', '..', 'file'), 'w'):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if os.path.exists(os.path.join(testpath, 'file')):
|
||||||
|
# Windows collapses 'current\..' to '.' first, leaving
|
||||||
|
# 'testpath\file'
|
||||||
|
dotdot_resolves_early = True
|
||||||
|
elif os.path.exists(os.path.join(testpath, '..', 'file')):
|
||||||
|
# Posix resolves 'current' to '.' first, leaving
|
||||||
|
# 'testpath/../file'
|
||||||
|
dotdot_resolves_early = False
|
||||||
|
else:
|
||||||
|
raise AssertionError('Could not determine link resolution')
|
||||||
|
|
||||||
with ArchiveMaker() as arc:
|
with ArchiveMaker() as arc:
|
||||||
|
|
||||||
|
# `current` links to `.` which is both the destination directory
|
||||||
|
# and `current` itself
|
||||||
arc.add('current', symlink_to='.')
|
arc.add('current', symlink_to='.')
|
||||||
|
|
||||||
|
# `current/parent` is also available as `./parent`,
|
||||||
|
# and effectively points to `./../`
|
||||||
arc.add('current/parent', symlink_to='..')
|
arc.add('current/parent', symlink_to='..')
|
||||||
|
|
||||||
arc.add('parent/evil')
|
arc.add('parent/evil')
|
||||||
|
|
||||||
with self.check_context(arc.open(), 'fully_trusted'):
|
with self.check_context(arc.open(), 'fully_trusted'):
|
||||||
|
|
@ -3469,6 +3518,7 @@ def test_parent_symlink2(self):
|
||||||
|
|
||||||
with self.check_context(arc.open(), 'tar'):
|
with self.check_context(arc.open(), 'tar'):
|
||||||
if os_helper.can_symlink():
|
if os_helper.can_symlink():
|
||||||
|
# Fail when extracting a file outside destination
|
||||||
self.expect_exception(
|
self.expect_exception(
|
||||||
tarfile.OutsideDestinationError,
|
tarfile.OutsideDestinationError,
|
||||||
"'parent/evil' would be extracted to "
|
"'parent/evil' would be extracted to "
|
||||||
|
|
@ -3479,10 +3529,24 @@ def test_parent_symlink2(self):
|
||||||
self.expect_file('parent/evil')
|
self.expect_file('parent/evil')
|
||||||
|
|
||||||
with self.check_context(arc.open(), 'data'):
|
with self.check_context(arc.open(), 'data'):
|
||||||
|
if os_helper.can_symlink():
|
||||||
|
if dotdot_resolves_early:
|
||||||
|
# Fail when extracting a file outside destination
|
||||||
|
self.expect_exception(
|
||||||
|
tarfile.OutsideDestinationError,
|
||||||
|
"'parent/evil' would be extracted to "
|
||||||
|
+ """['"].*evil['"], which is outside """
|
||||||
|
+ "the destination")
|
||||||
|
else:
|
||||||
|
# Fail as soon as we have a symlink outside the destination
|
||||||
self.expect_exception(
|
self.expect_exception(
|
||||||
tarfile.LinkOutsideDestinationError,
|
tarfile.LinkOutsideDestinationError,
|
||||||
"""'current/parent' would link to ['"].*['"], """
|
"'current/parent' would link to "
|
||||||
+ "which is outside the destination")
|
+ """['"].*outerdir['"], which is outside """
|
||||||
|
+ "the destination")
|
||||||
|
else:
|
||||||
|
self.expect_file('current/')
|
||||||
|
self.expect_file('parent/evil')
|
||||||
|
|
||||||
@symlink_test
|
@symlink_test
|
||||||
def test_absolute_symlink(self):
|
def test_absolute_symlink(self):
|
||||||
|
|
@ -3512,12 +3576,30 @@ def test_absolute_symlink(self):
|
||||||
with self.check_context(arc.open(), 'data'):
|
with self.check_context(arc.open(), 'data'):
|
||||||
self.expect_exception(
|
self.expect_exception(
|
||||||
tarfile.AbsoluteLinkError,
|
tarfile.AbsoluteLinkError,
|
||||||
"'parent' is a symlink to an absolute path")
|
"'parent' is a link to an absolute path")
|
||||||
|
|
||||||
|
def test_absolute_hardlink(self):
|
||||||
|
# Test hardlink to an absolute path
|
||||||
|
# Inspired by 'dirsymlink' in https://github.com/jwilk/traversal-archives
|
||||||
|
with ArchiveMaker() as arc:
|
||||||
|
arc.add('parent', hardlink_to=self.outerdir / 'foo')
|
||||||
|
|
||||||
|
with self.check_context(arc.open(), 'fully_trusted'):
|
||||||
|
self.expect_exception(KeyError, ".*foo. not found")
|
||||||
|
|
||||||
|
with self.check_context(arc.open(), 'tar'):
|
||||||
|
self.expect_exception(KeyError, ".*foo. not found")
|
||||||
|
|
||||||
|
with self.check_context(arc.open(), 'data'):
|
||||||
|
self.expect_exception(
|
||||||
|
tarfile.AbsoluteLinkError,
|
||||||
|
"'parent' is a link to an absolute path")
|
||||||
|
|
||||||
@symlink_test
|
@symlink_test
|
||||||
def test_sly_relative0(self):
|
def test_sly_relative0(self):
|
||||||
# Inspired by 'relative0' in jwilk/traversal-archives
|
# Inspired by 'relative0' in jwilk/traversal-archives
|
||||||
with ArchiveMaker() as arc:
|
with ArchiveMaker() as arc:
|
||||||
|
# points to `../../tmp/moo`
|
||||||
arc.add('../moo', symlink_to='..//tmp/moo')
|
arc.add('../moo', symlink_to='..//tmp/moo')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -3568,6 +3650,56 @@ def test_sly_relative2(self):
|
||||||
+ """['"].*moo['"], which is outside the """
|
+ """['"].*moo['"], which is outside the """
|
||||||
+ "destination")
|
+ "destination")
|
||||||
|
|
||||||
|
@symlink_test
|
||||||
|
def test_deep_symlink(self):
|
||||||
|
# Test that symlinks and hardlinks inside a directory
|
||||||
|
# point to the correct file (`target` of size 3).
|
||||||
|
# If links aren't supported we get a copy of the file.
|
||||||
|
with ArchiveMaker() as arc:
|
||||||
|
arc.add('targetdir/target', size=3)
|
||||||
|
# a hardlink's linkname is relative to the archive
|
||||||
|
arc.add('linkdir/hardlink', hardlink_to=os.path.join(
|
||||||
|
'targetdir', 'target'))
|
||||||
|
# a symlink's linkname is relative to the link's directory
|
||||||
|
arc.add('linkdir/symlink', symlink_to=os.path.join(
|
||||||
|
'..', 'targetdir', 'target'))
|
||||||
|
|
||||||
|
for filter in 'tar', 'data', 'fully_trusted':
|
||||||
|
with self.check_context(arc.open(), filter):
|
||||||
|
self.expect_file('targetdir/target', size=3)
|
||||||
|
self.expect_file('linkdir/hardlink', size=3)
|
||||||
|
if os_helper.can_symlink():
|
||||||
|
self.expect_file('linkdir/symlink', size=3,
|
||||||
|
symlink_to='../targetdir/target')
|
||||||
|
else:
|
||||||
|
self.expect_file('linkdir/symlink', size=3)
|
||||||
|
|
||||||
|
@symlink_test
|
||||||
|
def test_chains(self):
|
||||||
|
# Test chaining of symlinks/hardlinks.
|
||||||
|
# Symlinks are created before the files they point to.
|
||||||
|
with ArchiveMaker() as arc:
|
||||||
|
arc.add('linkdir/symlink', symlink_to='hardlink')
|
||||||
|
arc.add('symlink2', symlink_to=os.path.join(
|
||||||
|
'linkdir', 'hardlink2'))
|
||||||
|
arc.add('targetdir/target', size=3)
|
||||||
|
arc.add('linkdir/hardlink', hardlink_to='targetdir/target')
|
||||||
|
arc.add('linkdir/hardlink2', hardlink_to='linkdir/symlink')
|
||||||
|
|
||||||
|
for filter in 'tar', 'data', 'fully_trusted':
|
||||||
|
with self.check_context(arc.open(), filter):
|
||||||
|
self.expect_file('targetdir/target', size=3)
|
||||||
|
self.expect_file('linkdir/hardlink', size=3)
|
||||||
|
self.expect_file('linkdir/hardlink2', size=3)
|
||||||
|
if os_helper.can_symlink():
|
||||||
|
self.expect_file('linkdir/symlink', size=3,
|
||||||
|
symlink_to='hardlink')
|
||||||
|
self.expect_file('symlink2', size=3,
|
||||||
|
symlink_to='linkdir/hardlink2')
|
||||||
|
else:
|
||||||
|
self.expect_file('linkdir/symlink', size=3)
|
||||||
|
self.expect_file('symlink2', size=3)
|
||||||
|
|
||||||
def test_modes(self):
|
def test_modes(self):
|
||||||
# Test how file modes are extracted
|
# Test how file modes are extracted
|
||||||
# (Note that the modes are ignored on platforms without working chmod)
|
# (Note that the modes are ignored on platforms without working chmod)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
:func:`tarfile.data_filter` now takes the location of symlinks into account
|
||||||
|
when determining their target, so it will no longer reject some valid
|
||||||
|
tarballs with ``LinkOutsideDestinationError``.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue