Skip to content

Commit b7c5feb

Browse files
committed
[4.2.x] Fixed CVE-2023-36053 -- Prevented potential ReDoS in EmailValidator and URLValidator.
Thanks Seokchan Yoon for reports.
1 parent 1ea1136 commit b7c5feb

File tree

11 files changed

+75
-16
lines changed

11 files changed

+75
-16
lines changed

django/core/validators.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,15 @@ class URLValidator(RegexValidator):
104104
message = _("Enter a valid URL.")
105105
schemes = ["http", "https", "ftp", "ftps"]
106106
unsafe_chars = frozenset("\t\r\n")
107+
max_length = 2048
107108

108109
def __init__(self, schemes=None, **kwargs):
109110
super().__init__(**kwargs)
110111
if schemes is not None:
111112
self.schemes = schemes
112113

113114
def __call__(self, value):
114-
if not isinstance(value, str):
115+
if not isinstance(value, str) or len(value) > self.max_length:
115116
raise ValidationError(self.message, code=self.code, params={"value": value})
116117
if self.unsafe_chars.intersection(value):
117118
raise ValidationError(self.message, code=self.code, params={"value": value})
@@ -203,7 +204,9 @@ def __init__(self, message=None, code=None, allowlist=None):
203204
self.domain_allowlist = allowlist
204205

205206
def __call__(self, value):
206-
if not value or "@" not in value:
207+
# The maximum length of an email is 320 characters per RFC 3696
208+
# section 3.
209+
if not value or "@" not in value or len(value) > 320:
207210
raise ValidationError(self.message, code=self.code, params={"value": value})
208211

209212
user_part, domain_part = value.rsplit("@", 1)

django/forms/fields.py

+3
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,9 @@ class EmailField(CharField):
609609
default_validators = [validators.validate_email]
610610

611611
def __init__(self, **kwargs):
612+
# The default maximum length of an email is 320 characters per RFC 3696
613+
# section 3.
614+
kwargs.setdefault("max_length", 320)
612615
super().__init__(strip=True, **kwargs)
613616

614617

docs/ref/forms/fields.txt

+6-1
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,12 @@ For each field, we describe the default widget used if you don't specify
592592
* Error message keys: ``required``, ``invalid``
593593

594594
Has the optional arguments ``max_length``, ``min_length``, and
595-
``empty_value`` which work just as they do for :class:`CharField`.
595+
``empty_value`` which work just as they do for :class:`CharField`. The
596+
``max_length`` argument defaults to 320 (see :rfc:`3696#section-3`).
597+
598+
.. versionchanged:: 3.2.20
599+
600+
The default value for ``max_length`` was changed to 320 characters.
596601

597602
``FileField``
598603
-------------

docs/ref/validators.txt

+24-1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ to, or in lieu of custom ``field.clean()`` methods.
133133
:param code: If not ``None``, overrides :attr:`code`.
134134
:param allowlist: If not ``None``, overrides :attr:`allowlist`.
135135

136+
An :class:`EmailValidator` ensures that a value looks like an email, and
137+
raises a :exc:`~django.core.exceptions.ValidationError` with
138+
:attr:`message` and :attr:`code` if it doesn't. Values longer than 320
139+
characters are always considered invalid.
140+
136141
.. attribute:: message
137142

138143
The error message used by
@@ -154,13 +159,19 @@ to, or in lieu of custom ``field.clean()`` methods.
154159
validation, so you'd need to add them to the ``allowlist`` as
155160
necessary.
156161

162+
.. versionchanged:: 3.2.20
163+
164+
In older versions, values longer than 320 characters could be
165+
considered valid.
166+
157167
``URLValidator``
158168
----------------
159169

160170
.. class:: URLValidator(schemes=None, regex=None, message=None, code=None)
161171

162172
A :class:`RegexValidator` subclass that ensures a value looks like a URL,
163-
and raises an error code of ``'invalid'`` if it doesn't.
173+
and raises an error code of ``'invalid'`` if it doesn't. Values longer than
174+
:attr:`max_length` characters are always considered invalid.
164175

165176
Loopback addresses and reserved IP spaces are considered valid. Literal
166177
IPv6 addresses (:rfc:`3986#section-3.2.2`) and Unicode domains are both
@@ -177,6 +188,18 @@ to, or in lieu of custom ``field.clean()`` methods.
177188

178189
.. _valid URI schemes: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
179190

191+
.. attribute:: max_length
192+
193+
.. versionadded:: 3.2.20
194+
195+
The maximum length of values that could be considered valid. Defaults
196+
to 2048 characters.
197+
198+
.. versionchanged:: 3.2.20
199+
200+
In older versions, values longer than 2048 characters could be
201+
considered valid.
202+
180203
``validate_email``
181204
------------------
182205

docs/releases/3.2.20.txt

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@ Django 3.2.20 release notes
66

77
Django 3.2.20 fixes a security issue with severity "moderate" in 3.2.19.
88

9-
...
9+
CVE-2023-36053: Potential regular expression denial of service vulnerability in ``EmailValidator``/``URLValidator``
10+
===================================================================================================================
11+
12+
``EmailValidator`` and ``URLValidator`` were subject to potential regular
13+
expression denial of service attack via a very large number of domain name
14+
labels of emails and URLs.

docs/releases/4.1.10.txt

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@ Django 4.1.10 release notes
66

77
Django 4.1.10 fixes a security issue with severity "moderate" in 4.1.9.
88

9-
...
9+
CVE-2023-36053: Potential regular expression denial of service vulnerability in ``EmailValidator``/``URLValidator``
10+
===================================================================================================================
11+
12+
``EmailValidator`` and ``URLValidator`` were subject to potential regular
13+
expression denial of service attack via a very large number of domain name
14+
labels of emails and URLs.

docs/releases/4.2.3.txt

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ Django 4.2.3 release notes
77
Django 4.2.3 fixes a security issue with severity "moderate" and several bugs
88
in 4.2.2.
99

10+
CVE-2023-36053: Potential regular expression denial of service vulnerability in ``EmailValidator``/``URLValidator``
11+
===================================================================================================================
12+
13+
``EmailValidator`` and ``URLValidator`` were subject to potential regular
14+
expression denial of service attack via a very large number of domain name
15+
labels of emails and URLs.
16+
1017
Bugfixes
1118
========
1219

tests/forms_tests/field_tests/test_emailfield.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
class EmailFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
99
def test_emailfield_1(self):
1010
f = EmailField()
11+
self.assertEqual(f.max_length, 320)
1112
self.assertWidgetRendersTo(
12-
f, '<input type="email" name="f" id="id_f" required>'
13+
f, '<input type="email" name="f" id="id_f" maxlength="320" required>'
1314
)
1415
with self.assertRaisesMessage(ValidationError, "'This field is required.'"):
1516
f.clean("")

tests/forms_tests/tests/test_deprecation_forms.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ class CommentForm(Form):
6565
'<p>Name: <input type="text" name="name" maxlength="50"></p>'
6666
'<div class="errorlist">'
6767
'<div class="error">Enter a valid email address.</div></div>'
68-
'<p>Email: <input type="email" name="email" value="invalid" required></p>'
68+
'<p>Email: <input type="email" name="email" value="invalid" '
69+
'maxlength="320" required></p>'
6970
'<div class="errorlist">'
7071
'<div class="error">This field is required.</div></div>'
7172
'<p>Comment: <input type="text" name="comment" required></p>',

tests/forms_tests/tests/test_forms.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,8 @@ class SignupForm(Form):
547547

548548
f = SignupForm(auto_id=False)
549549
self.assertHTMLEqual(
550-
str(f["email"]), '<input type="email" name="email" required>'
550+
str(f["email"]),
551+
'<input type="email" name="email" maxlength="320" required>',
551552
)
552553
self.assertHTMLEqual(
553554
str(f["get_spam"]), '<input type="checkbox" name="get_spam" required>'
@@ -556,7 +557,8 @@ class SignupForm(Form):
556557
f = SignupForm({"email": "test@example.com", "get_spam": True}, auto_id=False)
557558
self.assertHTMLEqual(
558559
str(f["email"]),
559-
'<input type="email" name="email" value="test@example.com" required>',
560+
'<input type="email" name="email" maxlength="320" value="test@example.com" '
561+
"required>",
560562
)
561563
self.assertHTMLEqual(
562564
str(f["get_spam"]),
@@ -3522,7 +3524,7 @@ class Person(Form):
35223524
<option value="false">No</option>
35233525
</select></li>
35243526
<li><label for="id_email">Email:</label>
3525-
<input type="email" name="email" id="id_email"></li>
3527+
<input type="email" name="email" id="id_email" maxlength="320"></li>
35263528
<li class="required error"><ul class="errorlist">
35273529
<li>This field is required.</li></ul>
35283530
<label class="required" for="id_age">Age:</label>
@@ -3544,7 +3546,7 @@ class Person(Form):
35443546
<option value="false">No</option>
35453547
</select></p>
35463548
<p><label for="id_email">Email:</label>
3547-
<input type="email" name="email" id="id_email"></p>
3549+
<input type="email" name="email" id="id_email" maxlength="320"></p>
35483550
<ul class="errorlist"><li>This field is required.</li></ul>
35493551
<p class="required error"><label class="required" for="id_age">Age:</label>
35503552
<input type="number" name="age" id="id_age" required></p>
@@ -3564,7 +3566,7 @@ class Person(Form):
35643566
<option value="false">No</option>
35653567
</select></td></tr>
35663568
<tr><th><label for="id_email">Email:</label></th><td>
3567-
<input type="email" name="email" id="id_email"></td></tr>
3569+
<input type="email" name="email" id="id_email" maxlength="320"></td></tr>
35683570
<tr class="required error"><th><label class="required" for="id_age">Age:</label></th>
35693571
<td><ul class="errorlist"><li>This field is required.</li></ul>
35703572
<input type="number" name="age" id="id_age" required></td></tr>""",
@@ -3579,7 +3581,7 @@ class Person(Form):
35793581
'<option value="unknown" selected>Unknown</option>'
35803582
'<option value="true">Yes</option><option value="false">No</option>'
35813583
'</select></div><div><label for="id_email">Email:</label>'
3582-
'<input type="email" name="email" id="id_email" /></div>'
3584+
'<input type="email" name="email" id="id_email" maxlength="320"/></div>'
35833585
'<div class="required error"><label for="id_age" class="required">Age:'
35843586
'</label><ul class="errorlist"><li>This field is required.</li></ul>'
35853587
'<input type="number" name="age" required id="id_age" /></div>',
@@ -5056,8 +5058,9 @@ class CommentForm(Form):
50565058
'<p>Name: <input type="text" name="name" maxlength="50"></p>'
50575059
'<div class="errorlist">'
50585060
'<div class="error">Enter a valid email address.</div></div>'
5059-
'<p>Email: <input type="email" name="email" value="invalid" required></p>'
5060-
'<div class="errorlist">'
5061+
"<p>Email: "
5062+
'<input type="email" name="email" value="invalid" maxlength="320" required>'
5063+
'</p><div class="errorlist">'
50615064
'<div class="error">This field is required.</div></div>'
50625065
'<p>Comment: <input type="text" name="comment" required></p>',
50635066
)

tests/validators/tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
"ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
107107
"ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
108108
"ddddddddddddddddd:password@example.com:8080",
109+
"http://userid:password" + "d" * 2000 + "@example.aaaaaaaaaaaaa.com",
109110
"http://142.42.1.1/",
110111
"http://142.42.1.1:8080/",
111112
"http://➡.ws/䨹",
@@ -236,6 +237,7 @@
236237
"aaaaaa.com",
237238
"http://example.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
238239
"aaaaaa",
240+
"http://example." + ("a" * 63 + ".") * 1000 + "com",
239241
"http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaa."
240242
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaa"
241243
"aaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaa"
@@ -291,6 +293,7 @@
291293
(validate_email, "example@%s.%s.atm" % ("a" * 63, "b" * 10), None),
292294
(validate_email, "example@atm.%s" % ("a" * 64), ValidationError),
293295
(validate_email, "example@%s.atm.%s" % ("b" * 64, "a" * 63), ValidationError),
296+
(validate_email, "example@%scom" % (("a" * 63 + ".") * 100), ValidationError),
294297
(validate_email, None, ValidationError),
295298
(validate_email, "", ValidationError),
296299
(validate_email, "abc", ValidationError),

0 commit comments

Comments
 (0)