8000 [3.12] gh-122905: Sanitize names in zipfile.Path. (GH-122906) (#122923) · python/cpython@dcc5182 · GitHub
[go: up one dir, main page]

Skip to content

Commit dcc5182

Browse files
[3.12] gh-122905: Sanitize names in zipfile.Path. (GH-122906) (#122923)
1 parent 92ad3be commit dcc5182

File tree

3 files changed

+81
-1
lines changed

3 files changed

+81
-1
lines changed

Lib/test/test_zipfile/_path/test_path.py

Lines changed: 17 additions & 0 deletions
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

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,69 @@ def __setstate__(self, state):
8383
super().__init__(*args, **kwargs)
8484

8585

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