Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Doc/library/tarfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,10 @@ reused in custom filters:
paths (in case the name is absolute
even after stripping slashes, e.g. ``C:/foo`` on Windows).
This raises :class:`~tarfile.AbsolutePathError`.
- Normalize filenames (:attr:`TarInfo.name`) that contain ``..`` components
using :func:`os.path.normpath`.
Note that this removes internal ``..`` components, which may change the
meaning of the name if it traverses symbolic links.
- :ref:`Refuse <tarfile-extraction-refuse>` to extract files whose absolute
path (after following symlinks) would end up outside the destination.
This raises :class:`~tarfile.OutsideDestinationError`.
Expand All @@ -958,6 +962,10 @@ reused in custom filters:

Return the modified ``TarInfo`` member.

.. versionchanged:: next

Filenames containing ``..`` components are now normalized.

.. function:: data_filter(/, member, path)

Implements the ``'data'`` filter.
Expand Down
7 changes: 7 additions & 0 deletions Lib/tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,13 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
# For example, 'C:/foo' on Windows.
raise AbsolutePathError(member)
# Ensure we stay in the destination
if '..' in name.replace(os.sep, '/').split('/'):
# Directories are created from the name as given, so a name that
# leaves the destination part-way through would create them
# outside it even if the resolved path stays inside.
normalized = os.path.normpath(name)
if normalized != name:
name = new_attrs['name'] = normalized
target_path = os.path.realpath(os.path.join(dest_path, name),
strict=os.path.ALLOW_MISSING)
if os.path.commonpath([target_path, dest_path]) != dest_path:
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -3535,6 +3535,20 @@ def test_absolute(self):
tarfile.AbsolutePathError,
"""['"].*escaped.evil['"] has an absolute path""")

def test_parent_dir_out_and_back(self):
# Test a member that leaves the destination and comes back.
# The containment check looks at the resolved path, which stays
# inside, but the intermediate directories are created from the
# name as given, which does not.
with ArchiveMaker() as arc:
arc.add(f'../escaped.evil/../{self.destdir.name}/sub/file',
content='content')

for filter in 'tar', 'data':
with self.subTest(filter):
with self.check_context(arc.open(), filter):
self.expect_file('sub/file', content='content')

def test_parent_symlink(self):
# Test interplaying symlinks
# Inspired by 'dirsymlink2a' in jwilk/traversal-archives
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix the :mod:`tarfile` ``tar`` and ``data`` extraction filters creating
directories outside the destination for members whose name leaves the
destination and returns to it, such as ``../evil/../dest/sub/file``. The
containment check used the resolved path, but intermediate directories were
created from the name as given.
Loading