diff options
Diffstat (limited to 'meta-python/recipes-devtools/python/python3-django/CVE-2023-31047.patch')
-rw-r--r-- | meta-python/recipes-devtools/python/python3-django/CVE-2023-31047.patch | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/meta-python/recipes-devtools/python/python3-django/CVE-2023-31047.patch b/meta-python/recipes-devtools/python/python3-django/CVE-2023-31047.patch new file mode 100644 index 0000000000..ab29a2ed97 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-django/CVE-2023-31047.patch | |||
@@ -0,0 +1,352 @@ | |||
1 | From fd3215dec5d50aa1f09cb1f8eba193524e7379f3 Mon Sep 17 00:00:00 2001 | ||
2 | From: Mariusz Felisiak <felisiak.mariusz@gmail.com> | ||
3 | Date: Thu, 25 May 2023 14:49:15 +0000 | ||
4 | Subject: [PATCH] Fixed CVE-2023-31047, Fixed #31710 | ||
5 | |||
6 | -- Prevented potential bypass of validation when uploading multiple files using one form field. | ||
7 | |||
8 | Thanks Moataz Al-Sharida and nawaik for reports. | ||
9 | |||
10 | Co-authored-by: Shai Berger <shai@platonix.com> | ||
11 | Co-authored-by: nessita <124304+nessita@users.noreply.github.com> | ||
12 | |||
13 | CVE: CVE-2023-31047 | ||
14 | |||
15 | Upstream-Status: Backport [https://github.com/django/django/commit/fb4c55d9ec4bb812a7fb91fa20510d91645e411b] | ||
16 | |||
17 | Signed-off-by: Narpat Mali <narpat.mali@windriver.com> | ||
18 | --- | ||
19 | django/forms/widgets.py | 26 ++++++- | ||
20 | docs/releases/2.2.28.txt | 18 +++++ | ||
21 | docs/topics/http/file-uploads.txt | 65 ++++++++++++++++-- | ||
22 | .../forms_tests/field_tests/test_filefield.py | 68 ++++++++++++++++++- | ||
23 | .../widget_tests/test_clearablefileinput.py | 5 ++ | ||
24 | .../widget_tests/test_fileinput.py | 44 ++++++++++++ | ||
25 | 6 files changed, 218 insertions(+), 8 deletions(-) | ||
26 | |||
27 | diff --git a/django/forms/widgets.py b/django/forms/widgets.py | ||
28 | index e37036c..d0cc131 100644 | ||
29 | --- a/django/forms/widgets.py | ||
30 | +++ b/django/forms/widgets.py | ||
31 | @@ -372,17 +372,41 @@ class MultipleHiddenInput(HiddenInput): | ||
32 | |||
33 | |||
34 | class FileInput(Input): | ||
35 | + allow_multiple_selected = False | ||
36 | input_type = 'file' | ||
37 | needs_multipart_form = True | ||
38 | template_name = 'django/forms/widgets/file.html' | ||
39 | |||
40 | + def __init__(self, attrs=None): | ||
41 | + if ( | ||
42 | + attrs is not None | ||
43 | + and not self.allow_multiple_selected | ||
44 | + and attrs.get("multiple", False) | ||
45 | + ): | ||
46 | + raise ValueError( | ||
47 | + "%s doesn't support uploading multiple files." | ||
48 | + % self.__class__.__qualname__ | ||
49 | + ) | ||
50 | + if self.allow_multiple_selected: | ||
51 | + if attrs is None: | ||
52 | + attrs = {"multiple": True} | ||
53 | + else: | ||
54 | + attrs.setdefault("multiple", True) | ||
55 | + super().__init__(attrs) | ||
56 | + | ||
57 | def format_value(self, value): | ||
58 | """File input never renders a value.""" | ||
59 | return | ||
60 | |||
61 | def value_from_datadict(self, data, files, name): | ||
62 | "File widgets take data from FILES, not POST" | ||
63 | - return files.get(name) | ||
64 | + getter = files.get | ||
65 | + if self.allow_multiple_selected: | ||
66 | + try: | ||
67 | + getter = files.getlist | ||
68 | + except AttributeError: | ||
69 | + pass | ||
70 | + return getter(name) | ||
71 | |||
72 | def value_omitted_from_data(self, data, files, name): | ||
73 | return name not in files | ||
74 | diff --git a/docs/releases/2.2.28.txt b/docs/releases/2.2.28.txt | ||
75 | index 43270fc..854c6b0 100644 | ||
76 | --- a/docs/releases/2.2.28.txt | ||
77 | +++ b/docs/releases/2.2.28.txt | ||
78 | @@ -20,3 +20,21 @@ CVE-2022-28347: Potential SQL injection via ``QuerySet.explain(**options)`` on P | ||
79 | :meth:`.QuerySet.explain` method was subject to SQL injection in option names, | ||
80 | using a suitably crafted dictionary, with dictionary expansion, as the | ||
81 | ``**options`` argument. | ||
82 | + | ||
83 | +Backporting the CVE-2023-31047 fix on Django 2.2.28. | ||
84 | + | ||
85 | +CVE-2023-31047: Potential bypass of validation when uploading multiple files using one form field | ||
86 | +================================================================================================= | ||
87 | + | ||
88 | +Uploading multiple files using one form field has never been supported by | ||
89 | +:class:`.forms.FileField` or :class:`.forms.ImageField` as only the last | ||
90 | +uploaded file was validated. Unfortunately, :ref:`uploading_multiple_files` | ||
91 | +topic suggested otherwise. | ||
92 | + | ||
93 | +In order to avoid the vulnerability, :class:`~django.forms.ClearableFileInput` | ||
94 | +and :class:`~django.forms.FileInput` form widgets now raise ``ValueError`` when | ||
95 | +the ``multiple`` HTML attribute is set on them. To prevent the exception and | ||
96 | +keep the old behavior, set ``allow_multiple_selected`` to ``True``. | ||
97 | + | ||
98 | +For more details on using the new attribute and handling of multiple files | ||
99 | +through a single field, see :ref:`uploading_multiple_files`. | ||
100 | diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt | ||
101 | index 21a6f06..c1ffb80 100644 | ||
102 | --- a/docs/topics/http/file-uploads.txt | ||
103 | +++ b/docs/topics/http/file-uploads.txt | ||
104 | @@ -127,19 +127,54 @@ field in the model:: | ||
105 | form = UploadFileForm() | ||
106 | return render(request, 'upload.html', {'form': form}) | ||
107 | |||
108 | +.. _uploading_multiple_files: | ||
109 | + | ||
110 | Uploading multiple files | ||
111 | ------------------------ | ||
112 | |||
113 | -If you want to upload multiple files using one form field, set the ``multiple`` | ||
114 | -HTML attribute of field's widget: | ||
115 | +.. | ||
116 | + Tests in tests.forms_tests.field_tests.test_filefield.MultipleFileFieldTest | ||
117 | + should be updated after any changes in the following snippets. | ||
118 | + | ||
119 | +If you want to upload multiple files using one form field, create a subclass | ||
120 | +of the field's widget and set the ``allow_multiple_selected`` attribute on it | ||
121 | +to ``True``. | ||
122 | + | ||
123 | +In order for such files to be all validated by your form (and have the value of | ||
124 | +the field include them all), you will also have to subclass ``FileField``. See | ||
125 | +below for an example. | ||
126 | + | ||
127 | +.. admonition:: Multiple file field | ||
128 | + | ||
129 | + Django is likely to have a proper multiple file field support at some point | ||
130 | + in the future. | ||
131 | |||
132 | .. code-block:: python | ||
133 | :caption: forms.py | ||
134 | |||
135 | from django import forms | ||
136 | |||
137 | + | ||
138 | + class MultipleFileInput(forms.ClearableFileInput): | ||
139 | + allow_multiple_selected = True | ||
140 | + | ||
141 | + | ||
142 | + class MultipleFileField(forms.FileField): | ||
143 | + def __init__(self, *args, **kwargs): | ||
144 | + kwargs.setdefault("widget", MultipleFileInput()) | ||
145 | + super().__init__(*args, **kwargs) | ||
146 | + | ||
147 | + def clean(self, data, initial=None): | ||
148 | + single_file_clean = super().clean | ||
149 | + if isinstance(data, (list, tuple)): | ||
150 | + result = [single_file_clean(d, initial) for d in data] | ||
151 | + else: | ||
152 | + result = single_file_clean(data, initial) | ||
153 | + return result | ||
154 | + | ||
155 | + | ||
156 | class FileFieldForm(forms.Form): | ||
157 | - file_field = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True})) | ||
158 | + file_field = MultipleFileField() | ||
159 | |||
160 | Then override the ``post`` method of your | ||
161 | :class:`~django.views.generic.edit.FormView` subclass to handle multiple file | ||
162 | @@ -159,14 +194,32 @@ uploads: | ||
163 | def post(self, request, *args, **kwargs): | ||
164 | form_class = self.get_form_class() | ||
165 | form = self.get_form(form_class) | ||
166 | - files = request.FILES.getlist('file_field') | ||
167 | if form.is_valid(): | ||
168 | - for f in files: | ||
169 | - ... # Do something with each file. | ||
170 | return self.form_valid(form) | ||
171 | else: | ||
172 | return self.form_invalid(form) | ||
173 | |||
174 | + def form_valid(self, form): | ||
175 | + files = form.cleaned_data["file_field"] | ||
176 | + for f in files: | ||
177 | + ... # Do something with each file. | ||
178 | + return super().form_valid() | ||
179 | + | ||
180 | +.. warning:: | ||
181 | + | ||
182 | + This will allow you to handle multiple files at the form level only. Be | ||
183 | + aware that you cannot use it to put multiple files on a single model | ||
184 | + instance (in a single field), for example, even if the custom widget is used | ||
185 | + with a form field related to a model ``FileField``. | ||
186 | + | ||
187 | +.. backportedfix:: 2.2.28 | ||
188 | + | ||
189 | + In previous versions, there was no support for the ``allow_multiple_selected`` | ||
190 | + class attribute, and users were advised to create the widget with the HTML | ||
191 | + attribute ``multiple`` set through the ``attrs`` argument. However, this | ||
192 | + caused validation of the form field to be applied only to the last file | ||
193 | + submitted, which could have adverse security implications. | ||
194 | + | ||
195 | Upload Handlers | ||
196 | =============== | ||
197 | |||
198 | diff --git a/tests/forms_tests/field_tests/test_filefield.py b/tests/forms_tests/field_tests/test_filefield.py | ||
199 | index 3357444..ba559ee 100644 | ||
200 | --- a/tests/forms_tests/field_tests/test_filefield.py | ||
201 | +++ b/tests/forms_tests/field_tests/test_filefield.py | ||
202 | @@ -1,7 +1,8 @@ | ||
203 | import pickle | ||
204 | |||
205 | from django.core.files.uploadedfile import SimpleUploadedFile | ||
206 | -from django.forms import FileField, ValidationError | ||
207 | +from django.core.validators import validate_image_file_extension | ||
208 | +from django.forms import FileField, FileInput, ValidationError | ||
209 | from django.test import SimpleTestCase | ||
210 | |||
211 | |||
212 | @@ -82,3 +83,68 @@ class FileFieldTest(SimpleTestCase): | ||
213 | |||
214 | def test_file_picklable(self): | ||
215 | self.assertIsInstance(pickle.loads(pickle.dumps(FileField())), FileField) | ||
216 | + | ||
217 | + | ||
218 | +class MultipleFileInput(FileInput): | ||
219 | + allow_multiple_selected = True | ||
220 | + | ||
221 | + | ||
222 | +class MultipleFileField(FileField): | ||
223 | + def __init__(self, *args, **kwargs): | ||
224 | + kwargs.setdefault("widget", MultipleFileInput()) | ||
225 | + super().__init__(*args, **kwargs) | ||
226 | + | ||
227 | + def clean(self, data, initial=None): | ||
228 | + single_file_clean = super().clean | ||
229 | + if isinstance(data, (list, tuple)): | ||
230 | + result = [single_file_clean(d, initial) for d in data] | ||
231 | + else: | ||
232 | + result = single_file_clean(data, initial) | ||
233 | + return result | ||
234 | + | ||
235 | + | ||
236 | +class MultipleFileFieldTest(SimpleTestCase): | ||
237 | + def test_file_multiple(self): | ||
238 | + f = MultipleFileField() | ||
239 | + files = [ | ||
240 | + SimpleUploadedFile("name1", b"Content 1"), | ||
241 | + SimpleUploadedFile("name2", b"Content 2"), | ||
242 | + ] | ||
243 | + self.assertEqual(f.clean(files), files) | ||
244 | + | ||
245 | + def test_file_multiple_empty(self): | ||
246 | + f = MultipleFileField() | ||
247 | + files = [ | ||
248 | + SimpleUploadedFile("empty", b""), | ||
249 | + SimpleUploadedFile("nonempty", b"Some Content"), | ||
250 | + ] | ||
251 | + msg = "'The submitted file is empty.'" | ||
252 | + with self.assertRaisesMessage(ValidationError, msg): | ||
253 | + f.clean(files) | ||
254 | + with self.assertRaisesMessage(ValidationError, msg): | ||
255 | + f.clean(files[::-1]) | ||
256 | + | ||
257 | + def test_file_multiple_validation(self): | ||
258 | + f = MultipleFileField(validators=[validate_image_file_extension]) | ||
259 | + | ||
260 | + good_files = [ | ||
261 | + SimpleUploadedFile("image1.jpg", b"fake JPEG"), | ||
262 | + SimpleUploadedFile("image2.png", b"faux image"), | ||
263 | + SimpleUploadedFile("image3.bmp", b"fraudulent bitmap"), | ||
264 | + ] | ||
265 | + self.assertEqual(f.clean(good_files), good_files) | ||
266 | + | ||
267 | + evil_files = [ | ||
268 | + SimpleUploadedFile("image1.sh", b"#!/bin/bash -c 'echo pwned!'\n"), | ||
269 | + SimpleUploadedFile("image2.png", b"faux image"), | ||
270 | + SimpleUploadedFile("image3.jpg", b"fake JPEG"), | ||
271 | + ] | ||
272 | + | ||
273 | + evil_rotations = ( | ||
274 | + evil_files[i:] + evil_files[:i] # Rotate by i. | ||
275 | + for i in range(len(evil_files)) | ||
276 | + ) | ||
277 | + msg = "File extension “sh” is not allowed. Allowed extensions are: " | ||
278 | + for rotated_evil_files in evil_rotations: | ||
279 | + with self.assertRaisesMessage(ValidationError, msg): | ||
280 | + f.clean(rotated_evil_files) | ||
281 | diff --git a/tests/forms_tests/widget_tests/test_clearablefileinput.py b/tests/forms_tests/widget_tests/test_clearablefileinput.py | ||
282 | index 2ba376d..8d9e38a 100644 | ||
283 | --- a/tests/forms_tests/widget_tests/test_clearablefileinput.py | ||
284 | +++ b/tests/forms_tests/widget_tests/test_clearablefileinput.py | ||
285 | @@ -161,3 +161,8 @@ class ClearableFileInputTest(WidgetTest): | ||
286 | self.assertIs(widget.value_omitted_from_data({}, {}, 'field'), True) | ||
287 | self.assertIs(widget.value_omitted_from_data({}, {'field': 'x'}, 'field'), False) | ||
288 | self.assertIs(widget.value_omitted_from_data({'field-clear': 'y'}, {}, 'field'), False) | ||
289 | + | ||
290 | + def test_multiple_error(self): | ||
291 | + msg = "ClearableFileInput doesn't support uploading multiple files." | ||
292 | + with self.assertRaisesMessage(ValueError, msg): | ||
293 | + ClearableFileInput(attrs={"multiple": True}) | ||
294 | diff --git a/tests/forms_tests/widget_tests/test_fileinput.py b/tests/forms_tests/widget_tests/test_fileinput.py | ||
295 | index bbd7c7f..24daf5d 100644 | ||
296 | --- a/tests/forms_tests/widget_tests/test_fileinput.py | ||
297 | +++ b/tests/forms_tests/widget_tests/test_fileinput.py | ||
298 | @@ -1,4 +1,6 @@ | ||
299 | +from django.core.files.uploadedfile import SimpleUploadedFile | ||
300 | from django.forms import FileInput | ||
301 | +from django.utils.datastructures import MultiValueDict | ||
302 | |||
303 | from .base import WidgetTest | ||
304 | |||
305 | @@ -18,3 +20,45 @@ class FileInputTest(WidgetTest): | ||
306 | def test_value_omitted_from_data(self): | ||
307 | self.assertIs(self.widget.value_omitted_from_data({}, {}, 'field'), True) | ||
308 | self.assertIs(self.widget.value_omitted_from_data({}, {'field': 'value'}, 'field'), False) | ||
309 | + | ||
310 | + def test_multiple_error(self): | ||
311 | + msg = "FileInput doesn't support uploading multiple files." | ||
312 | + with self.assertRaisesMessage(ValueError, msg): | ||
313 | + FileInput(attrs={"multiple": True}) | ||
314 | + | ||
315 | + def test_value_from_datadict_multiple(self): | ||
316 | + class MultipleFileInput(FileInput): | ||
317 | + allow_multiple_selected = True | ||
318 | + | ||
319 | + file_1 = SimpleUploadedFile("something1.txt", b"content 1") | ||
320 | + file_2 = SimpleUploadedFile("something2.txt", b"content 2") | ||
321 | + # Uploading multiple files is allowed. | ||
322 | + widget = MultipleFileInput(attrs={"multiple": True}) | ||
323 | + value = widget.value_from_datadict( | ||
324 | + data={"name": "Test name"}, | ||
325 | + files=MultiValueDict({"myfile": [file_1, file_2]}), | ||
326 | + name="myfile", | ||
327 | + ) | ||
328 | + self.assertEqual(value, [file_1, file_2]) | ||
329 | + # Uploading multiple files is not allowed. | ||
330 | + widget = FileInput() | ||
331 | + value = widget.value_from_datadict( | ||
332 | + data={"name": "Test name"}, | ||
333 | + files=MultiValueDict({"myfile": [file_1, file_2]}), | ||
334 | + name="myfile", | ||
335 | + ) | ||
336 | + self.assertEqual(value, file_2) | ||
337 | + | ||
338 | + def test_multiple_default(self): | ||
339 | + class MultipleFileInput(FileInput): | ||
340 | + allow_multiple_selected = True | ||
341 | + | ||
342 | + tests = [ | ||
343 | + (None, True), | ||
344 | + ({"class": "myclass"}, True), | ||
345 | + ({"multiple": False}, False), | ||
346 | + ] | ||
347 | + for attrs, expected in tests: | ||
348 | + with self.subTest(attrs=attrs): | ||
349 | + widget = MultipleFileInput(attrs=attrs) | ||
350 | + self.assertIs(widget.attrs["multiple"], expected) | ||
351 | -- | ||
352 | 2.40.0 | ||