Skip to content

Commit 5aa989c

Browse files
[3.11] gh-155999: tarfile: handle a member that leaves the destination but comes back (GH-156000) (#156044)
gh-155999: `tarfile`: handle a member that leaves the destination but comes back (GH-156000) (cherry picked from commit 9768834) Co-authored-by: Stan Ulbrych <stan@python.org>
1 parent 95355ee commit 5aa989c

4 files changed

Lines changed: 34 additions & 0 deletions

File tree

‎Doc/library/tarfile.rst‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,10 @@ reused in custom filters:
10331033
paths (in case the name is absolute
10341034
even after stripping slashes, e.g. ``C:/foo`` on Windows).
10351035
This raises :class:`~tarfile.AbsolutePathError`.
1036+
- Normalize filenames (:attr:`TarInfo.name`) that contain ``..`` components
1037+
using :func:`os.path.normpath`.
1038+
Note that this removes internal ``..`` components, which may change the
1039+
meaning of the name if it traverses symbolic links.
10361040
- :ref:`Refuse <tarfile-extraction-refuse>` to extract files whose absolute
10371041
path (after following symlinks) would end up outside the destination.
10381042
This raises :class:`~tarfile.OutsideDestinationError`.
@@ -1041,6 +1045,10 @@ reused in custom filters:
10411045

10421046
Return the modified ``TarInfo`` member.
10431047

1048+
.. versionchanged:: next
1049+
1050+
Filenames containing ``..`` components are now normalized.
1051+
10441052
.. function:: data_filter(member, path)
10451053

10461054
Implements the ``'data'`` filter.

‎Lib/tarfile.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,13 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
778778
# For example, 'C:/foo' on Windows.
779779
raise AbsolutePathError(member)
780780
# Ensure we stay in the destination
781+
if '..' in name.replace(os.sep, '/').split('/'):
782+
# Directories are created from the name as given, so a name that
783+
# leaves the destination part-way through would create them
784+
# outside it even if the resolved path stays inside.
785+
normalized = os.path.normpath(name)
786+
if normalized != name:
787+
name = new_attrs['name'] = normalized
781788
target_path = os.path.realpath(os.path.join(dest_path, name),
782789
strict=os.path.ALLOW_MISSING)
783790
if os.path.commonpath([target_path, dest_path]) != dest_path:

‎Lib/test/test_tarfile.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3615,6 +3615,20 @@ def test_absolute(self):
36153615
tarfile.AbsolutePathError,
36163616
"""['"].*escaped.evil['"] has an absolute path""")
36173617

3618+
def test_parent_dir_out_and_back(self):
3619+
# Test a member that leaves the destination and comes back.
3620+
# The containment check looks at the resolved path, which stays
3621+
# inside, but the intermediate directories are created from the
3622+
# name as given, which does not.
3623+
with ArchiveMaker() as arc:
3624+
arc.add(f'../escaped.evil/../{self.destdir.name}/sub/file',
3625+
content='content')
3626+
3627+
for filter in 'tar', 'data':
3628+
with self.subTest(filter):
3629+
with self.check_context(arc.open(), filter):
3630+
self.expect_file('sub/file', content='content')
3631+
36183632
@symlink_test
36193633
def test_parent_symlink(self):
36203634
# Test interplaying symlinks
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix the :mod:`tarfile` ``tar`` and ``data`` extraction filters creating
2+
directories outside the destination for members whose name leaves the
3+
destination and returns to it, such as ``../evil/../dest/sub/file``. The
4+
containment check used the resolved path, but intermediate directories were
5+
created from the name as given.

0 commit comments

Comments
 (0)