diff options
Diffstat (limited to 'meta-python/recipes-devtools/python/python3-django')
| -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 | ||
