1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
From b1804347ec2db16452a7bff2b469d2c66776b904 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" <jaraco@jaraco.com>
Date: Fri, 31 May 2024 11:20:57 -0400
Subject: [PATCH] fix CVE-2024-5569
The patch includes the following changes:
c18417e Add news fragment.
58115d2 Employ SanitizedNames in CompleteDirs. Fixes broken test.
564fcc1 Add SanitizedNames mixin.
79a309f Add some assertions about malformed paths.
Upstream-Status: Backport
[https://github.com/jaraco/zipp/pull/120/commits/79a309fe54dc6b7934fb72e9f31bcb58f2e9f547]
[https://github.com/jaraco/zipp/pull/120/commits/564fcc10cdbfdaecdb33688e149827465931c9e0]
[https://github.com/jaraco/zipp/pull/120/commits/58115d2be968644ce71ce6bcc9b79826c82a1806]
[https://github.com/jaraco/zipp/pull/120/commits/c18417ed2953e181728a7dac07bff88a2190abf7]
CVE: CVE-2024-5569
Signed-off-by: Jiaying Song <jiaying.song.cn@windriver.com>
---
newsfragments/119.bugfix.rst | 1 +
tests/test_path.py | 17 ++++++++++
zipp/__init__.py | 64 +++++++++++++++++++++++++++++++++++-
3 files changed, 81 insertions(+), 1 deletion(-)
create mode 100644 newsfragments/119.bugfix.rst
diff --git a/newsfragments/119.bugfix.rst b/newsfragments/119.bugfix.rst
new file mode 100644
index 0000000..6c72e2d
--- /dev/null
+++ b/newsfragments/119.bugfix.rst
@@ -0,0 +1 @@
+Improved handling of malformed zip files.
\ No newline at end of file
diff --git a/tests/test_path.py b/tests/test_path.py
index a77a5de..3752243 100644
--- a/tests/test_path.py
+++ b/tests/test_path.py
@@ -575,3 +575,20 @@ class TestPath(unittest.TestCase):
zipp.Path(alpharep)
with self.assertRaises(KeyError):
alpharep.getinfo('does-not-exist')
+
+ def test_malformed_paths(self):
+ """
+ Path should handle malformed paths.
+ """
+ data = io.BytesIO()
+ zf = zipfile.ZipFile(data, "w")
+ zf.writestr("/one-slash.txt", b"content")
+ zf.writestr("//two-slash.txt", b"content")
+ zf.writestr("../parent.txt", b"content")
+ zf.filename = ''
+ root = zipfile.Path(zf)
+ assert list(map(str, root.iterdir())) == [
+ 'one-slash.txt',
+ 'two-slash.txt',
+ 'parent.txt',
+ ]
diff --git a/zipp/__init__.py b/zipp/__init__.py
index becd010..e980e9b 100644
--- a/zipp/__init__.py
+++ b/zipp/__init__.py
@@ -84,7 +84,69 @@ class InitializedState:
super().__init__(*args, **kwargs)
-class CompleteDirs(InitializedState, zipfile.ZipFile):
+class SanitizedNames:
+ """
+ ZipFile mix-in to ensure names are sanitized.
+ """
+
+ def namelist(self):
+ return list(map(self._sanitize, super().namelist()))
+
+ @staticmethod
+ def _sanitize(name):
+ r"""
+ Ensure a relative path with posix separators and no dot names.
+
+ Modeled after
+ https://github.com/python/cpython/blob/bcc1be39cb1d04ad9fc0bd1b9193d3972835a57c/Lib/zipfile/__init__.py#L1799-L1813
+ but provides consistent cross-platform behavior.
+
+ >>> san = SanitizedNames._sanitize
+ >>> san('/foo/bar')
+ 'foo/bar'
+ >>> san('//foo.txt')
+ 'foo.txt'
+ >>> san('foo/.././bar.txt')
+ 'foo/bar.txt'
+ >>> san('foo../.bar.txt')
+ 'foo../.bar.txt'
+ >>> san('\\foo\\bar.txt')
+ 'foo/bar.txt'
+ >>> san('D:\\foo.txt')
+ 'D/foo.txt'
+ >>> san('\\\\server\\share\\file.txt')
+ 'server/share/file.txt'
+ >>> san('\\\\?\\GLOBALROOT\\Volume3')
+ '?/GLOBALROOT/Volume3'
+ >>> san('\\\\.\\PhysicalDrive1\\root')
+ 'PhysicalDrive1/root'
+
+ Retain any trailing slash.
+ >>> san('abc/')
+ 'abc/'
+
+ Raises a ValueError if the result is empty.
+ >>> san('../..')
+ Traceback (most recent call last):
+ ...
+ ValueError: Empty filename
+ """
+
+ def allowed(part):
+ return part and part not in {'..', '.'}
+
+ # Remove the drive letter.
+ # Don't use ntpath.splitdrive, because that also strips UNC paths
+ bare = re.sub('^([A-Z]):', r'\1', name, flags=re.IGNORECASE)
+ clean = bare.replace('\\', '/')
+ parts = clean.split('/')
+ joined = '/'.join(filter(allowed, parts))
+ if not joined:
+ raise ValueError("Empty filename")
+ return joined + '/' * name.endswith('/')
+
+
+class CompleteDirs(InitializedState, SanitizedNames, zipfile.ZipFile):
"""
A ZipFile subclass that ensures that implied directories
are always included in the namelist.
--
2.25.1
|