Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit eed53d0

Browse files
felixxmshaibnessita
committed
[3.2.x] Fixed CVE-2023-31047, Fixed #31710 -- Prevented potential bypass of validation when uploading multiple files using one form field.
Thanks Moataz Al-Sharida and nawaik for reports. Co-authored-by: Shai Berger <[email protected]> Co-authored-by: nessita <[email protected]>
1 parent 007e46d commit eed53d0

File tree

6 files changed

+215
-9
lines changed

6 files changed

+215
-9
lines changed

django/forms/widgets.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,16 +378,40 @@ def format_value(self, value):
378378

379379
class FileInput(Input):
380380
input_type = 'file'
381+
allow_multiple_selected = False
381382
needs_multipart_form = True
382383
template_name = 'django/forms/widgets/file.html'
383384

385+
def __init__(self, attrs=None):
386+
if (
387+
attrs is not None and
388+
not self.allow_multiple_selected and
389+
attrs.get("multiple", False)
390+
):
391+
raise ValueError(
392+
"%s doesn't support uploading multiple files."
393+
% self.__class__.__qualname__
394+
)
395+
if self.allow_multiple_selected:
396+
if attrs is None:
397+
attrs = {"multiple": True}
398+
else:
399+
attrs.setdefault("multiple", True)
400+
super().__init__(attrs)
401+
384402
def format_value(self, value):
385403
"""File input never renders a value."""
386404
return
387405

388406
def value_from_datadict(self, data, files, name):
389407
"File widgets take data from FILES, not POST"
390-
return files.get(name)
408+
getter = files.get
409+
if self.allow_multiple_selected:
410+
try:
411+
getter = files.getlist
412+
except AttributeError:
413+
pass
414+
return getter(name)
391415

392416
def value_omitted_from_data(self, data, files, name):
393417
return name not in files

docs/releases/3.2.19.txt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,18 @@ Django 3.2.19 release notes
66

77
Django 3.2.19 fixes a security issue with severity "low" in 3.2.18.
88

9-
...
9+
CVE-2023-31047: Potential bypass of validation when uploading multiple files using one form field
10+
=================================================================================================
11+
12+
Uploading multiple files using one form field has never been supported by
13+
:class:`.forms.FileField` or :class:`.forms.ImageField` as only the last
14+
uploaded file was validated. Unfortunately, :ref:`uploading_multiple_files`
15+
topic suggested otherwise.
16+
17+
In order to avoid the vulnerability, :class:`~django.forms.ClearableFileInput`
18+
and :class:`~django.forms.FileInput` form widgets now raise ``ValueError`` when
19+
the ``multiple`` HTML attribute is set on them. To prevent the exception and
20+
keep the old behavior, set ``allow_multiple_selected`` to ``True``.
21+
22+
For more details on using the new attribute and handling of multiple files
23+
through a single field, see :ref:`uploading_multiple_files`.

docs/topics/http/file-uploads.txt

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,19 +126,54 @@ model::
126126
form = UploadFileForm()
127127
return render(request, 'upload.html', {'form': form})
128128

129+
.. _uploading_multiple_files:
130+
129131
Uploading multiple files
130132
------------------------
131133

132-
If you want to upload multiple files using one form field, set the ``multiple``
133-
HTML attribute of field's widget:
134+
..
135+
Tests in tests.forms_tests.field_tests.test_filefield.MultipleFileFieldTest
136+
should be updated after any changes in the following snippets.
137+
138+
If you want to upload multiple files using one form field, create a subclass
139+
of the field's widget and set the ``allow_multiple_selected`` attribute on it
140+
to ``True``.
141+
142+
In order for such files to be all validated by your form (and have the value of
143+
the field include them all), you will also have to subclass ``FileField``. See
144+
below for an example.
145+
146+
.. admonition:: Multiple file field
147+
148+
Django is likely to have a proper multiple file field support at some point
149+
in the future.
134150

135151
.. code-block:: python
136152
:caption: ``forms.py``
137153

138154
from django import forms
139155

156+
157+
class MultipleFileInput(forms.ClearableFileInput):
158+
allow_multiple_selected = True
159+
160+
161+
class MultipleFileField(forms.FileField):
162+
def __init__(self, *args, **kwargs):
163+
kwargs.setdefault("widget", MultipleFileInput())
164+
super().__init__(*args, **kwargs)
165+
166+
def clean(self, data, initial=None):
167+
single_file_clean = super().clean
168+
if isinstance(data, (list, tuple)):
169+
result = [single_file_clean(d, initial) for d in data]
170+
else:
171+
result = single_file_clean(data, initial)
172+
return result
173+
174+
140175
class FileFieldForm(forms.Form):
141-
file_field = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}))
176+
file_field = MultipleFileField()
142177

143178
Then override the ``post`` method of your
144179
:class:`~django.views.generic.edit.FormView` subclass to handle multiple file
@@ -158,14 +193,32 @@ uploads:
158193
def post(self, request, *args, **kwargs):
159194
form_class = self.get_form_class()
160195
form = self.get_form(form_class)
161-
files = request.FILES.getlist('file_field')
162196
if form.is_valid():
163-
for f in files:
164-
... # Do something with each file.
165197
return self.form_valid(form)
166198
else:
167199
return self.form_invalid(form)
168200

201+
def form_valid(self, form):
202+
files = form.cleaned_data["file_field"]
203+
for f in files:
204+
... # Do something with each file.
205+
return super().form_valid()
206+
207+
.. warning::
208+
209+
This will allow you to handle multiple files at the form level only. Be
210+
aware that you cannot use it to put multiple files on a single model
211+
instance (in a single field), for example, even if the custom widget is used
212+
with a form field related to a model ``FileField``.
213+
214+
.. versionchanged:: 3.2.19
215+
216+
In previous versions, there was no support for the ``allow_multiple_selected``
217+
class attribute, and users were advised to create the widget with the HTML
218+
attribute ``multiple`` set through the ``attrs`` argument. However, this
219+
caused validation of the form field to be applied only to the last file
220+
submitted, which could have adverse security implications.
221+
169222
Upload Handlers
170223
===============
171224

tests/forms_tests/field_tests/test_filefield.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from django.core.exceptions import ValidationError
44
from django.core.files.uploadedfile import SimpleUploadedFile
5-
from django.forms import FileField
5+
from django.core.validators import validate_image_file_extension
6+
from django.forms import FileField, FileInput
67
from django.test import SimpleTestCase
78

89

@@ -83,3 +84,68 @@ def test_disabled_has_changed(self):
8384

8485
def test_file_picklable(self):
8586
self.assertIsInstance(pickle.loads(pickle.dumps(FileField())), FileField)
87+
88+
89+
class MultipleFileInput(FileInput):
90+
allow_multiple_selected = True
91+
92+
93+
class MultipleFileField(FileField):
94+
def __init__(self, *args, **kwargs):
95+
kwargs.setdefault("widget", MultipleFileInput())
96+
super().__init__(*args, **kwargs)
97+
98+
def clean(self, data, initial=None):
99+
single_file_clean = super().clean
100+
if isinstance(data, (list, tuple)):
101+
result = [single_file_clean(d, initial) for d in data]
102+
else:
103+
result = single_file_clean(data, initial)
104+
return result
105+
106+
107+
class MultipleFileFieldTest(SimpleTestCase):
108+
def test_file_multiple(self):
109+
f = MultipleFileField()
110+
files = [
111+
SimpleUploadedFile("name1", b"Content 1"),
112+
SimpleUploadedFile("name2", b"Content 2"),
113+
]
114+
self.assertEqual(f.clean(files), files)
115+
116+
def test_file_multiple_empty(self):
117+
f = MultipleFileField()
118+
files = [
119+
SimpleUploadedFile("empty", b""),
120+
SimpleUploadedFile("nonempty", b"Some Content"),
121+
]
122+
msg = "'The submitted file is empty.'"
123+
with self.assertRaisesMessage(ValidationError, msg):
124+
f.clean(files)
125+
with self.assertRaisesMessage(ValidationError, msg):
126+
f.clean(files[::-1])
127+
128+
def test_file_multiple_validation(self):
129+
f = MultipleFileField(validators=[validate_image_file_extension])
130+
131+
good_files = [
132+
SimpleUploadedFile("image1.jpg", b"fake JPEG"),
133+
SimpleUploadedFile("image2.png", b"faux image"),
134+
SimpleUploadedFile("image3.bmp", b"fraudulent bitmap"),
135+
]
136+
self.assertEqual(f.clean(good_files), good_files)
137+
138+
evil_files = [
139+
SimpleUploadedFile("image1.sh", b"#!/bin/bash -c 'echo pwned!'\n"),
140+
SimpleUploadedFile("image2.png", b"faux image"),
141+
SimpleUploadedFile("image3.jpg", b"fake JPEG"),
142+
]
143+
144+
evil_rotations = (
145+
evil_files[i:] + evil_files[:i] # Rotate by i.
146+
for i in range(len(evil_files))
147+
)
148+
msg = "File extension “sh” is not allowed. Allowed extensions are: "
149+
for rotated_evil_files in evil_rotations:
150+
with self.assertRaisesMessage(ValidationError, msg):
151+
f.clean(rotated_evil_files)

tests/forms_tests/widget_tests/test_clearablefileinput.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,8 @@ def test_value_omitted_from_data(self):
176176
self.assertIs(widget.value_omitted_from_data({}, {}, 'field'), True)
177177
self.assertIs(widget.value_omitted_from_data({}, {'field': 'x'}, 'field'), False)
178178
self.assertIs(widget.value_omitted_from_data({'field-clear': 'y'}, {}, 'field'), False)
179+
180+
def test_multiple_error(self):
181+
msg = "ClearableFileInput doesn't support uploading multiple files."
182+
with self.assertRaisesMessage(ValueError, msg):
183+
ClearableFileInput(attrs={"multiple": True})

tests/forms_tests/widget_tests/test_fileinput.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
from django.core.files.uploadedfile import SimpleUploadedFile
12
from django.forms import FileInput
3+
from django.utils.datastructures import MultiValueDict
24

35
from .base import WidgetTest
46

@@ -24,3 +26,45 @@ def test_use_required_attribute(self):
2426
# user to keep the existing, initial value.
2527
self.assertIs(self.widget.use_required_attribute(None), True)
2628
self.assertIs(self.widget.use_required_attribute('resume.txt'), False)
29+
30+
def test_multiple_error(self):
31+
msg = "FileInput doesn't support uploading multiple files."
32+
with self.assertRaisesMessage(ValueError, msg):
33+
FileInput(attrs={"multiple": True})
34+
35+
def test_value_from_datadict_multiple(self):
36+
class MultipleFileInput(FileInput):
37+
allow_multiple_selected = True
38+
39+
file_1 = SimpleUploadedFile("something1.txt", b"content 1")
40+
file_2 = SimpleUploadedFile("something2.txt", b"content 2")
41+
# Uploading multiple files is allowed.
42+
widget = MultipleFileInput(attrs={"multiple": True})
43+
value = widget.value_from_datadict(
44+
data={"name": "Test name"},
45+
files=MultiValueDict({"myfile": [file_1, file_2]}),
46+
name="myfile",
47+
)
48+
self.assertEqual(value, [file_1, file_2])
49+
# Uploading multiple files is not allowed.
50+
widget = FileInput()
51+
value = widget.value_from_datadict(
52+
data={"name": "Test name"},
53+
files=MultiValueDict({"myfile": [file_1, file_2]}),
54+
name="myfile",
55+
)
56+
self.assertEqual(value, file_2)
57+
58+
def test_multiple_default(self):
59+
class MultipleFileInput(FileInput):
60+
allow_multiple_selected = True
61+
62+
tests = [
63+
(None, True),
64+
({"class": "myclass"}, True),
65+
({"multiple": False}, False),
66+
]
67+
for attrs, expected in tests:
68+
with self.subTest(attrs=attrs):
69+
widget = MultipleFileInput(attrs=attrs)
70+
self.assertIs(widget.attrs["multiple"], expected)

0 commit comments

Comments
 (0)