diff options
author | Chen Qi <Qi.Chen@windriver.com> | 2021-04-22 14:01:01 +0800 |
---|---|---|
committer | Khem Raj <raj.khem@gmail.com> | 2021-04-22 07:28:15 -0700 |
commit | eff7a55f492365e61ffabae4b9160885cba2afb4 (patch) | |
tree | 5c8e4d300d027cc8a39de84c303e23c4257d3dd1 /meta-python/recipes-devtools/python | |
parent | e0fe062ee45f19d7c9ea2765fd31d3bddddc2f2a (diff) | |
download | meta-openembedded-eff7a55f492365e61ffabae4b9160885cba2afb4.tar.gz |
python3-django: upgrade to 2.2.20
2.2.x is LTS, so upgrade to latest release 2.2.20.
This upgrade fixes several CVEs such as CVE-2021-3281.
Also, CVE-2021-28658.patch is dropped as it's already in 2.2.20.
Signed-off-by: Chen Qi <Qi.Chen@windriver.com>
Signed-off-by: Khem Raj <raj.khem@gmail.com>
Signed-off-by: Trevor Gamblin <trevor.gamblin@windriver.com>
Diffstat (limited to 'meta-python/recipes-devtools/python')
3 files changed, 9 insertions, 300 deletions
diff --git a/meta-python/recipes-devtools/python/python3-django-2.2.16/CVE-2021-28658.patch b/meta-python/recipes-devtools/python/python3-django-2.2.16/CVE-2021-28658.patch deleted file mode 100644 index 325aa00420..0000000000 --- a/meta-python/recipes-devtools/python/python3-django-2.2.16/CVE-2021-28658.patch +++ /dev/null | |||
@@ -1,289 +0,0 @@ | |||
1 | From 4036d62bda0e9e9f6172943794b744a454ca49c2 Mon Sep 17 00:00:00 2001 | ||
2 | From: Mariusz Felisiak <felisiak.mariusz@gmail.com> | ||
3 | Date: Tue, 16 Mar 2021 10:19:00 +0100 | ||
4 | Subject: [PATCH] Fixed CVE-2021-28658 -- Fixed potential directory-traversal | ||
5 | via uploaded files. | ||
6 | |||
7 | Thanks Claude Paroz for the initial patch. | ||
8 | Thanks Dennis Brinkrolf for the report. | ||
9 | |||
10 | Backport of d4d800ca1addc4141e03c5440a849bb64d1582cd from main. | ||
11 | |||
12 | Upstream-Status: Backport | ||
13 | CVE: CVE-2021-28658 | ||
14 | |||
15 | Reference to upstream patch: | ||
16 | [https://github.com/django/django/commit/4036d62bda0e9e9f6172943794b744a454ca49c2] | ||
17 | |||
18 | [SG: Adapted stable/2.2.x patch for 2.2.16] | ||
19 | Signed-off-by: Stefan Ghinea <stefan.ghinea@windriver.com> | ||
20 | --- | ||
21 | django/http/multipartparser.py | 13 ++++-- | ||
22 | docs/releases/2.2.16.txt | 12 +++++ | ||
23 | tests/file_uploads/tests.py | 72 ++++++++++++++++++++++------- | ||
24 | tests/file_uploads/uploadhandler.py | 31 +++++++++++++ | ||
25 | tests/file_uploads/urls.py | 1 + | ||
26 | tests/file_uploads/views.py | 12 ++++- | ||
27 | 6 files changed, 120 insertions(+), 21 deletions(-) | ||
28 | |||
29 | diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py | ||
30 | index f6f12ca..5a9cca8 100644 | ||
31 | --- a/django/http/multipartparser.py | ||
32 | +++ b/django/http/multipartparser.py | ||
33 | @@ -7,6 +7,7 @@ file upload handlers for processing. | ||
34 | import base64 | ||
35 | import binascii | ||
36 | import cgi | ||
37 | +import os | ||
38 | from urllib.parse import unquote | ||
39 | |||
40 | from django.conf import settings | ||
41 | @@ -205,7 +206,7 @@ class MultiPartParser: | ||
42 | file_name = disposition.get('filename') | ||
43 | if file_name: | ||
44 | file_name = force_text(file_name, encoding, errors='replace') | ||
45 | - file_name = self.IE_sanitize(unescape_entities(file_name)) | ||
46 | + file_name = self.sanitize_file_name(file_name) | ||
47 | if not file_name: | ||
48 | continue | ||
49 | |||
50 | @@ -293,9 +294,13 @@ class MultiPartParser: | ||
51 | self._files.appendlist(force_text(old_field_name, self._encoding, errors='replace'), file_obj) | ||
52 | break | ||
53 | |||
54 | - def IE_sanitize(self, filename): | ||
55 | - """Cleanup filename from Internet Explorer full paths.""" | ||
56 | - return filename and filename[filename.rfind("\\") + 1:].strip() | ||
57 | + def sanitize_file_name(self, file_name): | ||
58 | + file_name = unescape_entities(file_name) | ||
59 | + # Cleanup Windows-style path separators. | ||
60 | + file_name = file_name[file_name.rfind('\\') + 1:].strip() | ||
61 | + return os.path.basename(file_name) | ||
62 | + | ||
63 | + IE_sanitize = sanitize_file_name | ||
64 | |||
65 | def _close_files(self): | ||
66 | # Free up all file handles. | ||
67 | diff --git a/docs/releases/2.2.16.txt b/docs/releases/2.2.16.txt | ||
68 | index 31231fb..4b7021b 100644 | ||
69 | --- a/docs/releases/2.2.16.txt | ||
70 | +++ b/docs/releases/2.2.16.txt | ||
71 | @@ -2,6 +2,18 @@ | ||
72 | Django 2.2.16 release notes | ||
73 | =========================== | ||
74 | |||
75 | +*April 6, 2021* | ||
76 | + | ||
77 | +Backported from Django 2.2.20 a fix for a security issue. | ||
78 | + | ||
79 | +CVE-2021-28658: Potential directory-traversal via uploaded files | ||
80 | +================================================================ | ||
81 | + | ||
82 | +``MultiPartParser`` allowed directory-traversal via uploaded files with | ||
83 | +suitably crafted file names. | ||
84 | + | ||
85 | +Built-in upload handlers were not affected by this vulnerability. | ||
86 | + | ||
87 | *September 1, 2020* | ||
88 | |||
89 | Django 2.2.16 fixes two security issues and two data loss bugs in 2.2.15. | ||
90 | diff --git a/tests/file_uploads/tests.py b/tests/file_uploads/tests.py | ||
91 | index ea4976d..2a08d1b 100644 | ||
92 | --- a/tests/file_uploads/tests.py | ||
93 | +++ b/tests/file_uploads/tests.py | ||
94 | @@ -22,6 +22,21 @@ UNICODE_FILENAME = 'test-0123456789_中文_Orléans.jpg' | ||
95 | MEDIA_ROOT = sys_tempfile.mkdtemp() | ||
96 | UPLOAD_TO = os.path.join(MEDIA_ROOT, 'test_upload') | ||
97 | |||
98 | +CANDIDATE_TRAVERSAL_FILE_NAMES = [ | ||
99 | + '/tmp/hax0rd.txt', # Absolute path, *nix-style. | ||
100 | + 'C:\\Windows\\hax0rd.txt', # Absolute path, win-style. | ||
101 | + 'C:/Windows/hax0rd.txt', # Absolute path, broken-style. | ||
102 | + '\\tmp\\hax0rd.txt', # Absolute path, broken in a different way. | ||
103 | + '/tmp\\hax0rd.txt', # Absolute path, broken by mixing. | ||
104 | + 'subdir/hax0rd.txt', # Descendant path, *nix-style. | ||
105 | + 'subdir\\hax0rd.txt', # Descendant path, win-style. | ||
106 | + 'sub/dir\\hax0rd.txt', # Descendant path, mixed. | ||
107 | + '../../hax0rd.txt', # Relative path, *nix-style. | ||
108 | + '..\\..\\hax0rd.txt', # Relative path, win-style. | ||
109 | + '../..\\hax0rd.txt', # Relative path, mixed. | ||
110 | + '../hax0rd.txt', # HTML entities. | ||
111 | +] | ||
112 | + | ||
113 | |||
114 | @override_settings(MEDIA_ROOT=MEDIA_ROOT, ROOT_URLCONF='file_uploads.urls', MIDDLEWARE=[]) | ||
115 | class FileUploadTests(TestCase): | ||
116 | @@ -205,22 +220,8 @@ class FileUploadTests(TestCase): | ||
117 | # a malicious payload with an invalid file name (containing os.sep or | ||
118 | # os.pardir). This similar to what an attacker would need to do when | ||
119 | # trying such an attack. | ||
120 | - scary_file_names = [ | ||
121 | - "/tmp/hax0rd.txt", # Absolute path, *nix-style. | ||
122 | - "C:\\Windows\\hax0rd.txt", # Absolute path, win-style. | ||
123 | - "C:/Windows/hax0rd.txt", # Absolute path, broken-style. | ||
124 | - "\\tmp\\hax0rd.txt", # Absolute path, broken in a different way. | ||
125 | - "/tmp\\hax0rd.txt", # Absolute path, broken by mixing. | ||
126 | - "subdir/hax0rd.txt", # Descendant path, *nix-style. | ||
127 | - "subdir\\hax0rd.txt", # Descendant path, win-style. | ||
128 | - "sub/dir\\hax0rd.txt", # Descendant path, mixed. | ||
129 | - "../../hax0rd.txt", # Relative path, *nix-style. | ||
130 | - "..\\..\\hax0rd.txt", # Relative path, win-style. | ||
131 | - "../..\\hax0rd.txt" # Relative path, mixed. | ||
132 | - ] | ||
133 | - | ||
134 | payload = client.FakePayload() | ||
135 | - for i, name in enumerate(scary_file_names): | ||
136 | + for i, name in enumerate(CANDIDATE_TRAVERSAL_FILE_NAMES): | ||
137 | payload.write('\r\n'.join([ | ||
138 | '--' + client.BOUNDARY, | ||
139 | 'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i, name), | ||
140 | @@ -240,7 +241,7 @@ class FileUploadTests(TestCase): | ||
141 | response = self.client.request(**r) | ||
142 | # The filenames should have been sanitized by the time it got to the view. | ||
143 | received = response.json() | ||
144 | - for i, name in enumerate(scary_file_names): | ||
145 | + for i, name in enumerate(CANDIDATE_TRAVERSAL_FILE_NAMES): | ||
146 | got = received["file%s" % i] | ||
147 | self.assertEqual(got, "hax0rd.txt") | ||
148 | |||
149 | @@ -518,6 +519,36 @@ class FileUploadTests(TestCase): | ||
150 | # shouldn't differ. | ||
151 | self.assertEqual(os.path.basename(obj.testfile.path), 'MiXeD_cAsE.txt') | ||
152 | |||
153 | + def test_filename_traversal_upload(self): | ||
154 | + os.makedirs(UPLOAD_TO, exist_ok=True) | ||
155 | + self.addCleanup(shutil.rmtree, MEDIA_ROOT) | ||
156 | + file_name = '../test.txt', | ||
157 | + payload = client.FakePayload() | ||
158 | + payload.write( | ||
159 | + '\r\n'.join([ | ||
160 | + '--' + client.BOUNDARY, | ||
161 | + 'Content-Disposition: form-data; name="my_file"; ' | ||
162 | + 'filename="%s";' % file_name, | ||
163 | + 'Content-Type: text/plain', | ||
164 | + '', | ||
165 | + 'file contents.\r\n', | ||
166 | + '\r\n--' + client.BOUNDARY + '--\r\n', | ||
167 | + ]), | ||
168 | + ) | ||
169 | + r = { | ||
170 | + 'CONTENT_LENGTH': len(payload), | ||
171 | + 'CONTENT_TYPE': client.MULTIPART_CONTENT, | ||
172 | + 'PATH_INFO': '/upload_traversal/', | ||
173 | + 'REQUEST_METHOD': 'POST', | ||
174 | + 'wsgi.input': payload, | ||
175 | + } | ||
176 | + response = self.client.request(**r) | ||
177 | + result = response.json() | ||
178 | + self.assertEqual(response.status_code, 200) | ||
179 | + self.assertEqual(result['file_name'], 'test.txt') | ||
180 | + self.assertIs(os.path.exists(os.path.join(MEDIA_ROOT, 'test.txt')), False) | ||
181 | + self.assertIs(os.path.exists(os.path.join(UPLOAD_TO, 'test.txt')), True) | ||
182 | + | ||
183 | |||
184 | @override_settings(MEDIA_ROOT=MEDIA_ROOT) | ||
185 | class DirectoryCreationTests(SimpleTestCase): | ||
186 | @@ -591,6 +622,15 @@ class MultiParserTests(SimpleTestCase): | ||
187 | }, StringIO('x'), [], 'utf-8') | ||
188 | self.assertEqual(multipart_parser._content_length, 0) | ||
189 | |||
190 | + def test_sanitize_file_name(self): | ||
191 | + parser = MultiPartParser({ | ||
192 | + 'CONTENT_TYPE': 'multipart/form-data; boundary=_foo', | ||
193 | + 'CONTENT_LENGTH': '1' | ||
194 | + }, StringIO('x'), [], 'utf-8') | ||
195 | + for file_name in CANDIDATE_TRAVERSAL_FILE_NAMES: | ||
196 | + with self.subTest(file_name=file_name): | ||
197 | + self.assertEqual(parser.sanitize_file_name(file_name), 'hax0rd.txt') | ||
198 | + | ||
199 | def test_rfc2231_parsing(self): | ||
200 | test_data = ( | ||
201 | (b"Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A", | ||
202 | diff --git a/tests/file_uploads/uploadhandler.py b/tests/file_uploads/uploadhandler.py | ||
203 | index 7c6199f..65d70c6 100644 | ||
204 | --- a/tests/file_uploads/uploadhandler.py | ||
205 | +++ b/tests/file_uploads/uploadhandler.py | ||
206 | @@ -1,6 +1,8 @@ | ||
207 | """ | ||
208 | Upload handlers to test the upload API. | ||
209 | """ | ||
210 | +import os | ||
211 | +from tempfile import NamedTemporaryFile | ||
212 | |||
213 | from django.core.files.uploadhandler import FileUploadHandler, StopUpload | ||
214 | |||
215 | @@ -35,3 +37,32 @@ class ErroringUploadHandler(FileUploadHandler): | ||
216 | """A handler that raises an exception.""" | ||
217 | def receive_data_chunk(self, raw_data, start): | ||
218 | raise CustomUploadError("Oops!") | ||
219 | + | ||
220 | + | ||
221 | +class TraversalUploadHandler(FileUploadHandler): | ||
222 | + """A handler with potential directory-traversal vulnerability.""" | ||
223 | + def __init__(self, request=None): | ||
224 | + from .views import UPLOAD_TO | ||
225 | + | ||
226 | + super().__init__(request) | ||
227 | + self.upload_dir = UPLOAD_TO | ||
228 | + | ||
229 | + def file_complete(self, file_size): | ||
230 | + self.file.seek(0) | ||
231 | + self.file.size = file_size | ||
232 | + with open(os.path.join(self.upload_dir, self.file_name), 'wb') as fp: | ||
233 | + fp.write(self.file.read()) | ||
234 | + return self.file | ||
235 | + | ||
236 | + def new_file( | ||
237 | + self, field_name, file_name, content_type, content_length, charset=None, | ||
238 | + content_type_extra=None, | ||
239 | + ): | ||
240 | + super().new_file( | ||
241 | + file_name, file_name, content_length, content_length, charset, | ||
242 | + content_type_extra, | ||
243 | + ) | ||
244 | + self.file = NamedTemporaryFile(suffix='.upload', dir=self.upload_dir) | ||
245 | + | ||
246 | + def receive_data_chunk(self, raw_data, start): | ||
247 | + self.file.write(raw_data) | ||
248 | diff --git a/tests/file_uploads/urls.py b/tests/file_uploads/urls.py | ||
249 | index 3e7985d..eaac1da 100644 | ||
250 | --- a/tests/file_uploads/urls.py | ||
251 | +++ b/tests/file_uploads/urls.py | ||
252 | @@ -4,6 +4,7 @@ from . import views | ||
253 | |||
254 | urlpatterns = [ | ||
255 | path('upload/', views.file_upload_view), | ||
256 | + path('upload_traversal/', views.file_upload_traversal_view), | ||
257 | path('verify/', views.file_upload_view_verify), | ||
258 | path('unicode_name/', views.file_upload_unicode_name), | ||
259 | path('echo/', views.file_upload_echo), | ||
260 | diff --git a/tests/file_uploads/views.py b/tests/file_uploads/views.py | ||
261 | index d4947e4..137c6f3 100644 | ||
262 | --- a/tests/file_uploads/views.py | ||
263 | +++ b/tests/file_uploads/views.py | ||
264 | @@ -6,7 +6,9 @@ from django.http import HttpResponse, HttpResponseServerError, JsonResponse | ||
265 | |||
266 | from .models import FileModel | ||
267 | from .tests import UNICODE_FILENAME, UPLOAD_TO | ||
268 | -from .uploadhandler import ErroringUploadHandler, QuotaUploadHandler | ||
269 | +from .uploadhandler import ( | ||
270 | + ErroringUploadHandler, QuotaUploadHandler, TraversalUploadHandler, | ||
271 | +) | ||
272 | |||
273 | |||
274 | def file_upload_view(request): | ||
275 | @@ -158,3 +160,11 @@ def file_upload_fd_closing(request, access): | ||
276 | if access == 't': | ||
277 | request.FILES # Trigger file parsing. | ||
278 | return HttpResponse('') | ||
279 | + | ||
280 | + | ||
281 | +def file_upload_traversal_view(request): | ||
282 | + request.upload_handlers.insert(0, TraversalUploadHandler()) | ||
283 | + request.FILES # Trigger file parsing. | ||
284 | + return JsonResponse( | ||
285 | + {'file_name': request.upload_handlers[0].file_name}, | ||
286 | + ) | ||
287 | -- | ||
288 | 2.17.1 | ||
289 | |||
diff --git a/meta-python/recipes-devtools/python/python3-django_2.2.16.bb b/meta-python/recipes-devtools/python/python3-django_2.2.16.bb deleted file mode 100644 index eb626e8d3f..0000000000 --- a/meta-python/recipes-devtools/python/python3-django_2.2.16.bb +++ /dev/null | |||
@@ -1,11 +0,0 @@ | |||
1 | require python-django.inc | ||
2 | inherit setuptools3 | ||
3 | |||
4 | SRC_URI[md5sum] = "93faf5bbd54a19ea49f4932a813b9758" | ||
5 | SRC_URI[sha256sum] = "62cf45e5ee425c52e411c0742e641a6588b7e8af0d2c274a27940931b2786594" | ||
6 | |||
7 | RDEPENDS_${PN} += "\ | ||
8 | ${PYTHON_PN}-sqlparse \ | ||
9 | " | ||
10 | SRC_URI += "file://CVE-2021-28658.patch \ | ||
11 | " | ||
diff --git a/meta-python/recipes-devtools/python/python3-django_2.2.20.bb b/meta-python/recipes-devtools/python/python3-django_2.2.20.bb new file mode 100644 index 0000000000..905d022a4f --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-django_2.2.20.bb | |||
@@ -0,0 +1,9 @@ | |||
1 | require python-django.inc | ||
2 | inherit setuptools3 | ||
3 | |||
4 | SRC_URI[md5sum] = "947060d96ccc0a05e8049d839e541b25" | ||
5 | SRC_URI[sha256sum] = "2569f9dc5f8e458a5e988b03d6b7a02bda59b006d6782f4ea0fd590ed7336a64" | ||
6 | |||
7 | RDEPENDS_${PN} += "\ | ||
8 | ${PYTHON_PN}-sqlparse \ | ||
9 | " | ||