From 92cfb2fee8263f8c0c53e75b16a4f95176bd43af Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 13 Aug 2026 13:12:54 +0100 Subject: [PATCH 1/4] Handle a member that leaves the destination and comes back --- Lib/tarfile.py | 7 +++++++ Lib/test/test_tarfile.py | 18 ++++++++++++++++++ ...26-08-13-13-08-11.gh-issue-XXXXX.Xt4rWq.rst | 5 +++++ 3 files changed, 30 insertions(+) create mode 100644 Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-XXXXX.Xt4rWq.rst diff --git a/Lib/tarfile.py b/Lib/tarfile.py index dc5c3a59744cbc4..406247d350368c1 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -833,6 +833,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 5fa97e2ac226c43..d63fb57d332b6e3 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -4025,6 +4025,24 @@ 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') + + with self.check_context(arc.open(), 'fully_trusted'): + self.expect_file('../escaped.evil', type=tarfile.DIRTYPE) + self.expect_file('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-XXXXX.Xt4rWq.rst b/Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-XXXXX.Xt4rWq.rst new file mode 100644 index 000000000000000..59b725e55bbffda --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-XXXXX.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. From 03116fc1b22eea81d0f6293892640a55bf99d1ce Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Mon, 17 Aug 2026 18:57:32 +0100 Subject: [PATCH 2/4] Improve documentation --- Doc/library/tarfile.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Doc/library/tarfile.rst b/Doc/library/tarfile.rst index fc352e901f31dc7..b9a0914a52c4894 100644 --- a/Doc/library/tarfile.rst +++ b/Doc/library/tarfile.rst @@ -1107,6 +1107,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 ` to extract files whose absolute path (after following symlinks) would end up outside the destination. This raises :class:`~tarfile.OutsideDestinationError`. @@ -1115,6 +1119,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. From b2e29a9250ca7ea80e1ccf42f050d10f76007f1d Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 18 Aug 2026 12:24:05 +0100 Subject: [PATCH 3/4] Add issue number --- ....Xt4rWq.rst => 2026-08-13-13-08-11.gh-issue-155999.Xt4rWq.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/Security/{2026-08-13-13-08-11.gh-issue-XXXXX.Xt4rWq.rst => 2026-08-13-13-08-11.gh-issue-155999.Xt4rWq.rst} (100%) diff --git a/Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-XXXXX.Xt4rWq.rst b/Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-155999.Xt4rWq.rst similarity index 100% rename from Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-XXXXX.Xt4rWq.rst rename to Misc/NEWS.d/next/Security/2026-08-13-13-08-11.gh-issue-155999.Xt4rWq.rst From 40c0c7ce35070ced6a31bbf7d50fd3244e1a776b Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Tue, 18 Aug 2026 18:36:18 +0100 Subject: [PATCH 4/4] Drop test failing on Windows, it wasn't doing much anyway --- Lib/test/test_tarfile.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index d63fb57d332b6e3..8b647fcf21de8f0 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -4034,10 +4034,6 @@ def test_parent_dir_out_and_back(self): arc.add(f'../escaped.evil/../{self.destdir.name}/sub/file', content='content') - with self.check_context(arc.open(), 'fully_trusted'): - self.expect_file('../escaped.evil', type=tarfile.DIRTYPE) - self.expect_file('sub/file', content='content') - for filter in 'tar', 'data': with self.subTest(filter): with self.check_context(arc.open(), filter):