[3.14] gh-155999: `tarfile`: handle a member that leaves the destination but comes back (GH-156000) (#156041)

StanFromIreland <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/16dea1e887ec7dfbed735beedd476b21dcc91a79
commit: 16dea1e887ec7dfbed735beedd476b21dcc91a79
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: StanFromIreland <[email protected]>
date: 2026-08-19T13:29:34Z
summary:

[3.14] gh-155999: `tarfile`: handle a member that leaves the destination but comes back (GH-156000) (#156041)

(cherry picked from commit 97688346ada2df3e5b9c279348862c3d64ab0823)

Co-authored-by: Stan Ulbrych <[email protected]>

files:
A Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-155999.Xt4rWq.rst
M Doc/library/tarfile.rst
M Lib/tarfile.py
M Lib/test/test_tarfile.py

diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst
index 28e950e8f728e2..bbb913eec89f8b 100644
--- a/Doc/library/tarfile.rst
+++ b/Doc/library/tarfile.rst
@@ -1100,6 +1100,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`.
@@ -1108,6 +1112,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.
diff --git a/Lib/tarfile.py b/Lib/tarfile.py
index fe1f438ef1131c..f226a6ee4a5709 100644
--- a/Lib/tarfile.py
+++ b/Lib/tarfile.py
@@ -819,6 +819,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:
diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py
index 5f952aa8bef089..92954cc8c65294 100644
--- a/Lib/test/test_tarfile.py
+++ b/Lib/test/test_tarfile.py
@@ -3981,6 +3981,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')
+
     @symlink_test
     def test_parent_symlink(self):
         # Test interplaying symlinks
diff --git a/Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-155999.Xt4rWq.rst b/Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-155999.Xt4rWq.rst
new file mode 100644
index 00000000000000..59b725e55bbffd
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-155999.Xt4rWq.rst
@@ -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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.