Skip to content

Commit 21b1b1f

Browse files
felixxmshaibnessita
committed
[4.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 290fd5e commit 21b1b1f

File tree

8 files changed

+245
-12
lines changed

8 files changed

+245
-12
lines changed

django/forms/widgets.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,17 +413,41 @@ def format_value(self, value):
413413

414414

415415
class FileInput(Input):
416+
allow_multiple_selected = False
416417
input_type = "file"
417418
needs_multipart_form = True
418419
template_name = "django/forms/widgets/file.html"
419420

421+
def __init__(self, attrs=None):
422+
if (
423+
attrs is not None
424+
and not self.allow_multiple_selected
425+
and attrs.get("multiple", False)
426+
):
427+
raise ValueError(
428+
"%s doesn't support uploading multiple files."
429+
% self.__class__.__qualname__
430+
)
431+
if self.allow_multiple_selected:
432+
if attrs is None:
433+
attrs = {"multiple": True}
434+
else:
435+
attrs.setdefault("multiple", True)
436+
super().__init__(attrs)
437+
420438
def format_value(self, value):
421439
"""File input never renders a value."""
422440
return
423441

424442
def value_from_datadict(self, data, files, name):
425443
"File widgets take data from FILES, not POST"
426-
return files.get(name)
444+
getter = files.get
445+
if self.allow_multiple_selected:
446+
try:
447+
getter = files.getlist
448+
except AttributeError:
449+
pass
450+
return getter(name)
427451

428452
def value_omitted_from_data(self, data, files, name):
429453
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/releases/4.1.9.txt

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

77
Django 4.1.9 fixes a security issue with severity "low" in 4.1.8.
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/releases/4.2.1.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ Django 4.2.1 release notes
77
Django 4.2.1 fixes a security issue with severity "low" and several bugs in
88
4.2.
99

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

docs/topics/http/file-uploads.txt

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,22 +144,54 @@ a :class:`~django.core.files.File` like object to the
144144
instance = ModelWithFileField(file_field=content_file)
145145
instance.save()
146146

147+
.. _uploading_multiple_files:
148+
147149
Uploading multiple files
148150
------------------------
149151

150-
If you want to upload multiple files using one form field, set the ``multiple``
151-
HTML attribute of field's widget:
152+
..
153+
Tests in tests.forms_tests.field_tests.test_filefield.MultipleFileFieldTest
154+
should be updated after any changes in the following snippets.
155+
156+
If you want to upload multiple files using one form field, create a subclass
157+
of the field's widget and set the ``allow_multiple_selected`` attribute on it
158+
to ``True``.
159+
160+
In order for such files to be all validated by your form (and have the value of
161+
the field include them all), you will also have to subclass ``FileField``. See
162+
below for an example.
163+
164+
.. admonition:: Multiple file field
165+
166+
Django is likely to have a proper multiple file field support at some point
167+
in the future.
152168

153169
.. code-block:: python
154170
:caption: ``forms.py``
155171

156172
from django import forms
157173

158174

175+
class MultipleFileInput(forms.ClearableFileInput):
176+
allow_multiple_selected = True
177+
178+
179+
class MultipleFileField(forms.FileField):
180+
def __init__(self, *args, **kwargs):
181+
kwargs.setdefault("widget", MultipleFileInput())
182+
super().__init__(*args, **kwargs)
183+
184+
def clean(self, data, initial=None):
185+
single_file_clean = super().clean
186+
if isinstance(data, (list, tuple)):
187+
result = [single_file_clean(d, initial) for d in data]
188+
else:
189+
result = single_file_clean(data, initial)
190+
return result
191+
192+
159193
class FileFieldForm(forms.Form):
160-
file_field = forms.FileField(
161-
widget=forms.ClearableFileInput(attrs={"multiple": True})
162-
)
194+
file_field = MultipleFileField()
163195

164196
Then override the ``post`` method of your
165197
:class:`~django.views.generic.edit.FormView` subclass to handle multiple file
@@ -180,14 +212,32 @@ uploads:
180212
def post(self, request, *args, **kwargs):
181213
form_class = self.get_form_class()
182214
form = self.get_form(form_class)
183-
files = request.FILES.getlist("file_field")
184215
if form.is_valid():
185-
for f in files:
186-
... # Do something with each file.
187216
return self.form_valid(form)
188217
else:
189218
return self.form_invalid(form)
190219

220+
def form_valid(self, form):
221+
files = form.cleaned_data["file_field"]
222+
for f in files:
223+
... # Do something with each file.
224+
return super().form_valid()
225+
226+
.. warning::
227+
228+
This will allow you to handle multiple files at the form level only. Be
229+
aware that you cannot use it to put multiple files on a single model
230+
instance (in a single field), for example, even if the custom widget is used
231+
with a form field related to a model ``FileField``.
232+
233+
.. versionchanged:: 3.2.19
234+
235+
In previous versions, there was no support for the ``allow_multiple_selected``
236+
class attribute, and users were advised to create the widget with the HTML
237+
attribute ``multiple`` set through the ``attrs`` argument. However, this
238+
caused validation of the form field to be applied only to the last file
239+
submitted, which could have adverse security implications.
240+
191241
Upload Handlers
192242
===============
193243

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

@@ -109,3 +110,68 @@ def test_disabled_has_changed(self):
109110

110111
def test_file_picklable(self):
111112
self.assertIsInstance(pickle.loads(pickle.dumps(FileField())), FileField)
113+
114+
115+
class MultipleFileInput(FileInput):
116+
allow_multiple_selected = True
117+
118+
119+
class MultipleFileField(FileField):
120+
def __init__(self, *args, **kwargs):
121+
kwargs.setdefault("widget", MultipleFileInput())
122+
super().__init__(*args, **kwargs)
123+
124+
def clean(self, data, initial=None):
125+
single_file_clean = super().clean
126+
if isinstance(data, (list, tuple)):
127+
result = [single_file_clean(d, initial) for d in data]
128+
else:
129+
result = single_file_clean(data, initial)
130+
return result
131+
132+
133+
class MultipleFileFieldTest(SimpleTestCase):
134+
def test_file_multiple(self):
135+
f = MultipleFileField()
136+
files = [
137+
SimpleUploadedFile("name1", b"Content 1"),
138+
SimpleUploadedFile("name2", b"Content 2"),
139+
]
140+
self.assertEqual(f.clean(files), files)
141+
142+
def test_file_multiple_empty(self):
143+
f = MultipleFileField()
144+
files = [
145+
SimpleUploadedFile("empty", b""),
146+
SimpleUploadedFile("nonempty", b"Some Content"),
147+
]
148+
msg = "'The submitted file is empty.'"
149+
with self.assertRaisesMessage(ValidationError, msg):
150+
f.clean(files)
151+
with self.assertRaisesMessage(ValidationError, msg):
152+
f.clean(files[::-1])
153+
154+
def test_file_multiple_validation(self):
155+
f = MultipleFileField(validators=[validate_image_file_extension])
156+
157+
good_files = [
158+
SimpleUploadedFile("image1.jpg", b"fake JPEG"),
159+
SimpleUploadedFile("image2.png", b"faux image"),
160+
SimpleUploadedFile("image3.bmp", b"fraudulent bitmap"),
161+
]
162+
self.assertEqual(f.clean(good_files), good_files)
163+
164+
evil_files = [
165+
SimpleUploadedFile("image1.sh", b"#!/bin/bash -c 'echo pwned!'\n"),
166+
SimpleUploadedFile("image2.png", b"faux image"),
167+
SimpleUploadedFile("image3.jpg", b"fake JPEG"),
168+
]
169+
170+
evil_rotations = (
171+
evil_files[i:] + evil_files[:i] # Rotate by i.
172+
for i in range(len(evil_files))
173+
)
174+
msg = "File extension “sh” is not allowed. Allowed extensions are: "
175+
for rotated_evil_files in evil_rotations:
176+
with self.assertRaisesMessage(ValidationError, msg):
177+
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
@@ -242,3 +242,8 @@ class TestForm(Form):
242242
'<input type="file" name="clearable_file" id="id_clearable_file"></div>',
243243
form.render(),
244244
)
245+
246+
def test_multiple_error(self):
247+
msg = "ClearableFileInput doesn't support uploading multiple files."
248+
with self.assertRaisesMessage(ValueError, msg):
249+
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 FileField, FileInput, Form
3+
from django.utils.datastructures import MultiValueDict
24

35
from .base import WidgetTest
46

@@ -48,3 +50,45 @@ class TestForm(Form):
4850
'name="field" required type="file"></div>',
4951
form.render(),
5052
)
53+
54+
def test_multiple_error(self):
55+
msg = "FileInput doesn't support uploading multiple files."
56+
with self.assertRaisesMessage(ValueError, msg):
57+
FileInput(attrs={"multiple": True})
58+
59+
def test_value_from_datadict_multiple(self):
60+
class MultipleFileInput(FileInput):
61+
allow_multiple_selected = True
62+
63+
file_1 = SimpleUploadedFile("something1.txt", b"content 1")
64+
file_2 = SimpleUploadedFile("something2.txt", b"content 2")
65+
# Uploading multiple files is allowed.
66+
widget = MultipleFileInput(attrs={"multiple": True})
67+
value = widget.value_from_datadict(
68+
data={"name": "Test name"},
69+
files=MultiValueDict({"myfile": [file_1, file_2]}),
70+
name="myfile",
71+
)
72+
self.assertEqual(value, [file_1, file_2])
73+
# Uploading multiple files is not allowed.
74+
widget = FileInput()
75+
value = widget.value_from_datadict(
76+
data={"name": "Test name"},
77+
files=MultiValueDict({"myfile": [file_1, file_2]}),
78+
name="myfile",
79+
)
80+
self.assertEqual(value, file_2)
81+
82+
def test_multiple_default(self):
83+
class MultipleFileInput(FileInput):
84+
allow_multiple_selected = True
85+
86+
tests = [
87+
(None, True),
88+
({"class": "myclass"}, True),
89+
({"multiple": False}, False),
90+
]
91+
for attrs, expected in tests:
92+
with self.subTest(attrs=attrs):
93+
widget = MultipleFileInput(attrs=attrs)
94+
self.assertIs(widget.attrs["multiple"], expected)

0 commit comments

Comments
 (0)