Skip to content

Commit 6ac089b

Browse files
miss-islingtongpsheadencukou
authored
[3.15] gh-161323: Refuse drive-relative paths in tarfile extraction filters (GH-161334) (#161355)
gh-161323: Refuse drive-relative paths in tarfile extraction filters (GH-161334) * gh-161323: Refuse drive-relative paths in tarfile extraction filters On Windows, os.path.join() does not put a name such as 'C:foo' below a destination that has no drive, so the 'tar' and 'data' filters now refuse names and link targets with a drive, after normalization. Fixes CVE-2026-107784. reported by Girish Konda ANT-2026-RSL1F4AT (cherry picked from commit e4c751b) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> Co-authored-by: Petr Viktorin <encukou@gmail.com>
1 parent b2e4251 commit 6ac089b

4 files changed

Lines changed: 69 additions & 11 deletions

File tree

‎Doc/library/tarfile.rst‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,14 +1103,15 @@ reused in custom filters:
11031103
Implements the ``'tar'`` filter.
11041104

11051105
- Strip leading slashes (``/`` and :data:`os.sep`) from filenames.
1106-
- :ref:`Refuse <tarfile-extraction-refuse>` to extract files with absolute
1107-
paths (in case the name is absolute
1108-
even after stripping slashes, e.g. ``C:/foo`` on Windows).
1109-
This raises :class:`~tarfile.AbsolutePathError`.
11101106
- Normalize filenames (:attr:`TarInfo.name`) that contain ``..`` components
11111107
using :func:`os.path.normpath`.
11121108
Note that this removes internal ``..`` components, which may change the
11131109
meaning of the name if it traverses symbolic links.
1110+
- :ref:`Refuse <tarfile-extraction-refuse>` to extract files with absolute
1111+
paths (in case the name is absolute
1112+
even after stripping slashes, e.g. ``C:/foo`` on Windows)
1113+
or with a drive (e.g. ``C:foo`` on Windows).
1114+
This raises :class:`~tarfile.AbsolutePathError`.
11141115
- :ref:`Refuse <tarfile-extraction-refuse>` to extract files whose absolute
11151116
path (after following symlinks) would end up outside the destination.
11161117
This raises :class:`~tarfile.OutsideDestinationError`.
@@ -1128,6 +1129,10 @@ reused in custom filters:
11281129

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

1132+
.. versionchanged:: next
1133+
1134+
Filenames with a drive that are not absolute are now refused.
1135+
11311136
.. function:: data_filter(member, path)
11321137

11331138
Implements the ``'data'`` filter.
@@ -1140,7 +1145,8 @@ reused in custom filters:
11401145
symbolic links.
11411146

11421147
- :ref:`Refuse <tarfile-extraction-refuse>` to extract links (hard or soft)
1143-
that link to absolute paths, or ones that link outside the destination.
1148+
that link to absolute paths or to paths with a drive,
1149+
or ones that link outside the destination.
11441150

11451151
This raises :class:`~tarfile.AbsoluteLinkError` or
11461152
:class:`~tarfile.LinkOutsideDestinationError`.
@@ -1174,6 +1180,10 @@ reused in custom filters:
11741180

11751181
Link targets are now normalized.
11761182

1183+
.. versionchanged:: next
1184+
1185+
Link targets with a drive that are not absolute are now refused.
1186+
11771187

11781188
.. _tarfile-extraction-refuse:
11791189

‎Lib/tarfile.py‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -832,10 +832,6 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
832832
# Include os.sep (target OS directory separator) as well.
833833
if name.startswith(('/', os.sep)):
834834
name = new_attrs['name'] = member.path.lstrip('/' + os.sep)
835-
if os.path.isabs(name):
836-
# Path is absolute even after stripping.
837-
# For example, 'C:/foo' on Windows.
838-
raise AbsolutePathError(member)
839835
# Ensure we stay in the destination
840836
if '..' in name.replace(os.sep, '/').split('/'):
841837
# Directories are created from the name as given, so a name that
@@ -844,6 +840,11 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
844840
normalized = os.path.normpath(name)
845841
if normalized != name:
846842
name = new_attrs['name'] = normalized
843+
if os.path.isabs(name) or os.path.splitdrive(name)[0]:
844+
# Path is absolute even after stripping, or has a drive.
845+
# For example, 'C:/foo' or 'C:foo' on Windows.
846+
# os.path.join() ignores a destination without a drive for 'C:foo'.
847+
raise AbsolutePathError(member)
847848
target_path = os.path.realpath(os.path.join(dest_path, name),
848849
strict=os.path.ALLOW_MISSING)
849850
if os.path.commonpath([target_path, dest_path]) != dest_path:
@@ -887,11 +888,11 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
887888
new_attrs['gname'] = None
888889
# Check link destination for 'data'
889890
if member.islnk() or member.issym():
890-
if os.path.isabs(member.linkname):
891-
raise AbsoluteLinkError(member)
892891
normalized = os.path.normpath(member.linkname)
893892
if normalized != member.linkname:
894893
new_attrs['linkname'] = normalized
894+
if os.path.isabs(normalized) or os.path.splitdrive(normalized)[0]:
895+
raise AbsoluteLinkError(member)
895896
if member.issym():
896897
# The symlink is created at `name` with trailing separators
897898
# stripped, so its target is relative to the directory

‎Lib/test/test_tarfile.py‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import sys
33
import os
44
import io
5+
import ntpath
56
from hashlib import sha256
67
from contextlib import contextmanager, ExitStack
78
from random import Random
@@ -4075,6 +4076,47 @@ def test_absolute(self):
40754076
tarfile.AbsolutePathError,
40764077
"""['"].*escaped.evil['"] has an absolute path""")
40774078

4079+
def test_drive_relative(self):
4080+
# Test names and link targets that have a drive but no root.
4081+
# They are not absolute, but os.path.join() does not put them
4082+
# below a destination that has no drive or another drive.
4083+
# Windows path handling is used on all platforms.
4084+
dest = r'C:\outerdir\dest'
4085+
with support.swap_attr(os, 'path', ntpath):
4086+
for name in 'C:escaped.evil', 'x/../C:escaped.evil':
4087+
for filter in tarfile.tar_filter, tarfile.data_filter:
4088+
with self.subTest(name=name, filter=filter.__name__):
4089+
with self.assertRaises(tarfile.AbsolutePathError):
4090+
filter(tarfile.TarInfo(name), dest)
4091+
4092+
for linkname in 'C:escaped.evil', 'x/../C:escaped.evil', './C:evil':
4093+
for type in tarfile.SYMTYPE, tarfile.LNKTYPE:
4094+
with self.subTest(linkname=linkname, type=type):
4095+
member = tarfile.TarInfo('link')
4096+
member.type = type
4097+
member.linkname = linkname
4098+
with self.assertRaises(tarfile.AbsoluteLinkError):
4099+
tarfile.data_filter(member, dest)
4100+
4101+
member = tarfile.TarInfo('x/../C/benign.txt')
4102+
member = tarfile.data_filter(member, dest)
4103+
self.assertEqual(member.name, r'C\benign.txt')
4104+
4105+
@unittest.skipUnless(os.name == 'nt', 'requires Windows drives')
4106+
def test_drive_relative_to_cwd(self):
4107+
# Test a name like 'C:escaped.evil' with a destination that has no
4108+
# drive. The file must not be created in the current directory.
4109+
with os_helper.temp_cwd() as cwd:
4110+
drive = os.path.splitdrive(cwd)[0]
4111+
with ArchiveMaker() as arc:
4112+
arc.add(f'{drive}escaped.evil')
4113+
4114+
for filter in 'tar', 'data':
4115+
with self.subTest(filter=filter), arc.open() as tar:
4116+
with self.assertRaises(tarfile.AbsolutePathError):
4117+
tar.extractall('dest', filter=filter)
4118+
self.assertFalse(os.path.lexists('escaped.evil'))
4119+
40784120
def test_parent_dir_out_and_back(self):
40794121
# Test a member that leaves the destination and comes back.
40804122
# The containment check looks at the resolved path, which stays
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
On Windows, the ``'tar'`` and ``'data'`` :mod:`tarfile` extraction filters
2+
now refuse member names and link targets that have a drive but are not
3+
absolute, such as ``C:foo``, with :exc:`~tarfile.AbsolutePathError` or
4+
:exc:`~tarfile.AbsoluteLinkError`. Such members could be extracted outside
5+
the destination directory. Fixes :cve:`2026-107784`.

0 commit comments

Comments
 (0)