Skip to content

Commit 9cd0326

Browse files
authored
gh-122905: Sanitize names in zipfile.Path. (#122906)
Ported from zipp 3.19.1; ref jaraco/zipp#119.
1 parent 4534068 commit 9cd0326

File tree

3 files changed

+81
-1
lines changed

3 files changed

+81
-1
lines changed

Lib/test/test_zipfile/_path/test_path.py

+17
Original file line numberDiff line numberDiff line change
@@ -577,3 +577,20 @@ def test_getinfo_missing(self, alpharep):
577577
zipfile.Path(alpharep)
578578
with self.assertRaises(KeyError):
579579
alpharep.getinfo('does-not-exist')
580+
581+
def test_malformed_paths(self):
582+
"""
583+
Path should handle malformed paths.
584+
"""
585+
data = io.BytesIO()
586+
zf = zipfile.ZipFile(data, "w")
587+
zf.writestr("/one-slash.txt", b"content")
588+
zf.writestr("//two-slash.txt", b"content")
589+
zf.writestr("../parent.txt", b"content")
590+
zf.filename = ''
591+
root = zipfile.Path(zf)
592+
assert list(map(str, root.iterdir())) == [
593+
'one-slash.txt',
594+
'two-slash.txt',
595+
'parent.txt',
596+
]

Lib/zipfile/_path/__init__.py

+63-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,69 @@ def __setstate__(self, state):
8585
super().__init__(*args, **kwargs)
8686

8787

88-
class CompleteDirs(InitializedState, zipfile.ZipFile):
88+
class SanitizedNames:
89+
"""
90+
ZipFile mix-in to ensure names are sanitized.
91+
"""
92+
93+
def namelist(self):
94+
return list(map(self._sanitize, super().namelist()))
95+
96+
@staticmethod
97+
def _sanitize(name):
98+
r"""
99+
Ensure a relative path with posix separators and no dot names.
100+
101+
Modeled after
102+
https://github.com/python/cpython/blob/bcc1be39cb1d04ad9fc0bd1b9193d3972835a57c/Lib/zipfile/__init__.py#L1799-L1813
103+
but provides consistent cross-platform behavior.
104+
105+
>>> san = SanitizedNames._sanitize
106+
>>> san('/foo/bar')
107+
'foo/bar'
108+
>>> san('//foo.txt')
109+
'foo.txt'
110+
>>> san('foo/.././bar.txt')
111+
'foo/bar.txt'
112+
>>> san('foo../.bar.txt')
113+
'foo../.bar.txt'
114+
>>> san('\\foo\\bar.txt')
115+
'foo/bar.txt'
116+
>>> san('D:\\foo.txt')
117+
'D/foo.txt'
118+
>>> san('\\\\server\\share\\file.txt')
119+
'server/share/file.txt'
120+
>>> san('\\\\?\\GLOBALROOT\\Volume3')
121+
'?/GLOBALROOT/Volume3'
122+
>>> san('\\\\.\\PhysicalDrive1\\root')
123+
'PhysicalDrive1/root'
124+
125+
Retain any trailing slash.
126+
>>> san('abc/')
127+
'abc/'
128+
129+
Raises a ValueError if the result is empty.
130+
>>> san('../..')
131+
Traceback (most recent call last):
132+
...
133+
ValueError: Empty filename
134+
"""
135+
136+
def allowed(part):
137+
return part and part not in {'..', '.'}
138+
139+
# Remove the drive letter.
140+
# Don't use ntpath.splitdrive, because that also strips UNC paths
141+
bare = re.sub('^([A-Z]):', r'\1', name, flags=re.IGNORECASE)
142+
clean = bare.replace('\\', '/')
143+
parts = clean.split('/')
144+
joined = '/'.join(filter(allowed, parts))
145+
if not joined:
146+
raise ValueError("Empty filename")
147+
return joined + '/' * name.endswith('/')
148+
149+
150+
class CompleteDirs(InitializedState, SanitizedNames, zipfile.ZipFile):
89151
"""
90152
A ZipFile subclass that ensures that implied directories
91153
are always included in the namelist.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:class:`zipfile.Path` objects now sanitize names from the zipfile.

0 commit comments

Comments
 (0)