1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
|
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Date: Wed, 14 Jun 2023 12:23:06 +0200
Subject: [PATCH] [3.2.x] Fixed CVE-2023-36053 -- Prevented potential ReDoS in
EmailValidator and URLValidator.
Thanks Seokchan Yoon for reports.
---
django/core/validators.py | 7 +++++--
django/forms/fields.py | 3 +++
docs/ref/forms/fields.txt | 7 ++++++-
docs/ref/validators.txt | 25 +++++++++++++++++++++++-
tests/forms_tests/field_tests/test_emailfield.py | 5 ++++-
tests/forms_tests/tests/test_forms.py | 19 ++++++++++++------
tests/validators/tests.py | 11 +++++++++++
7 files changed, 66 insertions(+), 11 deletions(-)
diff --git a/django/core/validators.py b/django/core/validators.py
index 6b28eef08dd2..52ebddac6345 100644
--- a/django/core/validators.py
+++ b/django/core/validators.py
@@ -93,6 +93,7 @@ class URLValidator(RegexValidator):
message = _('Enter a valid URL.')
schemes = ['http', 'https', 'ftp', 'ftps']
unsafe_chars = frozenset('\t\r\n')
+ max_length = 2048
def __init__(self, schemes=None, **kwargs):
super().__init__(**kwargs)
@@ -100,7 +101,7 @@ class URLValidator(RegexValidator):
self.schemes = schemes
def __call__(self, value):
- if not isinstance(value, str):
+ if not isinstance(value, str) or len(value) > self.max_length:
raise ValidationError(self.message, code=self.code, params={'value': value})
if self.unsafe_chars.intersection(value):
raise ValidationError(self.message, code=self.code, params={'value': value})
@@ -211,7 +212,9 @@ class EmailValidator:
self.domain_allowlist = allowlist
def __call__(self, value):
- if not value or '@' not in value:
+ # The maximum length of an email is 320 characters per RFC 3696
+ # section 3.
+ if not value or '@' not in value or len(value) > 320:
raise ValidationError(self.message, code=self.code, params={'value': value})
user_part, domain_part = value.rsplit('@', 1)
diff --git a/django/forms/fields.py b/django/forms/fields.py
index 0214d60c1cf1..8adb09e38294 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -540,6 +540,9 @@ class EmailField(CharField):
default_validators = [validators.validate_email]
def __init__(self, **kwargs):
+ # The default maximum length of an email is 320 characters per RFC 3696
+ # section 3.
+ kwargs.setdefault("max_length", 320)
super().__init__(strip=True, **kwargs)
diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
index 9438214a28ce..5b485f215384 100644
--- a/docs/ref/forms/fields.txt
+++ b/docs/ref/forms/fields.txt
@@ -592,7 +592,12 @@ For each field, we describe the default widget used if you don't specify
* Error message keys: ``required``, ``invalid``
Has three optional arguments ``max_length``, ``min_length``, and
- ``empty_value`` which work just as they do for :class:`CharField`.
+ ``empty_value`` which work just as they do for :class:`CharField`. The
+ ``max_length`` argument defaults to 320 (see :rfc:`3696#section-3`).
+
+ .. versionchanged:: 3.2.20
+
+ The default value for ``max_length`` was changed to 320 characters.
``FileField``
-------------
diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt
index 50761e5a425c..b22762b17b93 100644
--- a/docs/ref/validators.txt
+++ b/docs/ref/validators.txt
@@ -130,6 +130,11 @@ to, or in lieu of custom ``field.clean()`` methods.
:param code: If not ``None``, overrides :attr:`code`.
:param allowlist: If not ``None``, overrides :attr:`allowlist`.
+ An :class:`EmailValidator` ensures that a value looks like an email, and
+ raises a :exc:`~django.core.exceptions.ValidationError` with
+ :attr:`message` and :attr:`code` if it doesn't. Values longer than 320
+ characters are always considered invalid.
+
.. attribute:: message
The error message used by
@@ -158,13 +163,19 @@ to, or in lieu of custom ``field.clean()`` methods.
The undocumented ``domain_whitelist`` attribute is deprecated. Use
``domain_allowlist`` instead.
+ .. versionchanged:: 3.2.20
+
+ In older versions, values longer than 320 characters could be
+ considered valid.
+
``URLValidator``
----------------
.. class:: URLValidator(schemes=None, regex=None, message=None, code=None)
A :class:`RegexValidator` subclass that ensures a value looks like a URL,
- and raises an error code of ``'invalid'`` if it doesn't.
+ and raises an error code of ``'invalid'`` if it doesn't. Values longer than
+ :attr:`max_length` characters are always considered invalid.
Loopback addresses and reserved IP spaces are considered valid. Literal
IPv6 addresses (:rfc:`3986#section-3.2.2`) and Unicode domains are both
@@ -181,6 +192,18 @@ to, or in lieu of custom ``field.clean()`` methods.
.. _valid URI schemes: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
+ .. attribute:: max_length
+
+ .. versionadded:: 3.2.20
+
+ The maximum length of values that could be considered valid. Defaults
+ to 2048 characters.
+
+ .. versionchanged:: 3.2.20
+
+ In older versions, values longer than 2048 characters could be
+ considered valid.
+
``validate_email``
------------------
diff --git a/tests/forms_tests/field_tests/test_emailfield.py b/tests/forms_tests/field_tests/test_emailfield.py
index 8b85e4dcc144..19d315205d7e 100644
--- a/tests/forms_tests/field_tests/test_emailfield.py
+++ b/tests/forms_tests/field_tests/test_emailfield.py
@@ -9,7 +9,10 @@ class EmailFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
def test_emailfield_1(self):
f = EmailField()
- self.assertWidgetRendersTo(f, '<input type="email" name="f" id="id_f" required>')
+ self.assertEqual(f.max_length, 320)
+ self.assertWidgetRendersTo(
+ f, '<input type="email" name="f" id="id_f" maxlength="320" required>'
+ )
with self.assertRaisesMessage(ValidationError, "'This field is required.'"):
f.clean('')
with self.assertRaisesMessage(ValidationError, "'This field is required.'"):
diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index 26f8ecafea44..82a32af403a0 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -422,11 +422,18 @@ class FormsTestCase(SimpleTestCase):
get_spam = BooleanField()
f = SignupForm(auto_id=False)
- self.assertHTMLEqual(str(f['email']), '<input type="email" name="email" required>')
+ self.assertHTMLEqual(
+ str(f["email"]),
+ '<input type="email" name="email" maxlength="320" required>',
+ )
self.assertHTMLEqual(str(f['get_spam']), '<input type="checkbox" name="get_spam" required>')
f = SignupForm({'email': 'test@example.com', 'get_spam': True}, auto_id=False)
- self.assertHTMLEqual(str(f['email']), '<input type="email" name="email" value="test@example.com" required>')
+ self.assertHTMLEqual(
+ str(f["email"]),
+ '<input type="email" name="email" maxlength="320" value="test@example.com" '
+ "required>",
+ )
self.assertHTMLEqual(
str(f['get_spam']),
'<input checked type="checkbox" name="get_spam" required>',
@@ -2824,7 +2831,7 @@ Good luck picking a username that doesn't already exist.</p>
<option value="true">Yes</option>
<option value="false">No</option>
</select></li>
-<li><label for="id_email">Email:</label> <input type="email" name="email" id="id_email"></li>
+<li><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" maxlength="320"></li>
<li class="required error"><ul class="errorlist"><li>This field is required.</li></ul>
<label class="required" for="id_age">Age:</label> <input type="number" name="age" id="id_age" required></li>"""
)
@@ -2840,7 +2847,7 @@ Good luck picking a username that doesn't already exist.</p>
<option value="true">Yes</option>
<option value="false">No</option>
</select></p>
-<p><label for="id_email">Email:</label> <input type="email" name="email" id="id_email"></p>
+<p><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" maxlength="320"></p>
<ul class="errorlist"><li>This field is required.</li></ul>
<p class="required error"><label class="required" for="id_age">Age:</label>
<input type="number" name="age" id="id_age" required></p>"""
@@ -2859,7 +2866,7 @@ Good luck picking a username that doesn't already exist.</p>
<option value="false">No</option>
</select></td></tr>
<tr><th><label for="id_email">Email:</label></th><td>
-<input type="email" name="email" id="id_email"></td></tr>
+<input type="email" name="email" id="id_email" maxlength="320"></td></tr>
<tr class="required error"><th><label class="required" for="id_age">Age:</label></th>
<td><ul class="errorlist"><li>This field is required.</li></ul>
<input type="number" name="age" id="id_age" required></td></tr>"""
@@ -3489,7 +3496,7 @@ Good luck picking a username that doesn't already exist.</p>
f = CommentForm(data, auto_id=False, error_class=DivErrorList)
self.assertHTMLEqual(f.as_p(), """<p>Name: <input type="text" name="name" maxlength="50"></p>
<div class="errorlist"><div class="error">Enter a valid email address.</div></div>
-<p>Email: <input type="email" name="email" value="invalid" required></p>
+<p>Email: <input type="email" name="email" value="invalid" maxlength="320" required></p>
<div class="errorlist"><div class="error">This field is required.</div></div>
<p>Comment: <input type="text" name="comment" required></p>""")
diff --git a/tests/validators/tests.py b/tests/validators/tests.py
index e39d0e3a1cef..1065727a974e 100644
--- a/tests/validators/tests.py
+++ b/tests/validators/tests.py
@@ -59,6 +59,7 @@ TEST_DATA = [
(validate_email, 'example@atm.%s' % ('a' * 64), ValidationError),
(validate_email, 'example@%s.atm.%s' % ('b' * 64, 'a' * 63), ValidationError),
+ (validate_email, "example@%scom" % (("a" * 63 + ".") * 100), ValidationError),
(validate_email, None, ValidationError),
(validate_email, '', ValidationError),
(validate_email, 'abc', ValidationError),
@@ -246,6 +247,16 @@ TEST_DATA = [
(URLValidator(), None, ValidationError),
(URLValidator(), 56, ValidationError),
(URLValidator(), 'no_scheme', ValidationError),
+ (
+ URLValidator(),
+ "http://example." + ("a" * 63 + ".") * 1000 + "com",
+ ValidationError,
+ ),
+ (
+ URLValidator(),
+ "http://userid:password" + "d" * 2000 + "@example.aaaaaaaaaaaaa.com",
+ None,
+ ),
# Newlines and tabs are not accepted.
(URLValidator(), 'http://www.djangoproject.com/\n', ValidationError),
(URLValidator(), 'http://[::ffff:192.9.5.5]\n', ValidationError),
|