Skip to content

Commit 574dd5e

Browse files
committed
[1.8.x] Prevented newlines from being accepted in some validators.
This is a security fix; disclosure to follow shortly. Thanks to Sjoerd Job Postmus for the report and draft patch.
1 parent 66d12d1 commit 574dd5e

File tree

5 files changed

+111
-13
lines changed

5 files changed

+111
-13
lines changed

django/core/validators.py

+15-12
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class URLValidator(RegexValidator):
8383
r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')'
8484
r'(?::\d{2,5})?' # port
8585
r'(?:[/?#][^\s]*)?' # resource path
86-
r'$', re.IGNORECASE)
86+
r'\Z', re.IGNORECASE)
8787
message = _('Enter a valid URL.')
8888
schemes = ['http', 'https', 'ftp', 'ftps']
8989

@@ -125,30 +125,33 @@ def __call__(self, value):
125125
raise ValidationError(self.message, code=self.code)
126126
url = value
127127

128+
integer_validator = RegexValidator(
129+
re.compile('^-?\d+\Z'),
130+
message=_('Enter a valid integer.'),
131+
code='invalid',
132+
)
133+
128134

129135
def validate_integer(value):
130-
try:
131-
int(value)
132-
except (ValueError, TypeError):
133-
raise ValidationError(_('Enter a valid integer.'), code='invalid')
136+
return integer_validator(value)
134137

135138

136139
@deconstructible
137140
class EmailValidator(object):
138141
message = _('Enter a valid email address.')
139142
code = 'invalid'
140143
user_regex = re.compile(
141-
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$" # dot-atom
142-
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"$)', # quoted-string
144+
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z" # dot-atom
145+
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)', # quoted-string
143146
re.IGNORECASE)
144147
domain_regex = re.compile(
145148
# max length of the domain is 249: 254 (max email length) minus one
146149
# period, two characters for the TLD, @ sign, & one character before @.
147-
r'(?:[A-Z0-9](?:[A-Z0-9-]{0,247}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,}(?<!-))$',
150+
r'(?:[A-Z0-9](?:[A-Z0-9-]{0,247}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,}(?<!-))\Z',
148151
re.IGNORECASE)
149152
literal_regex = re.compile(
150153
# literal form, ipv4 or ipv6 address (SMTP 4.1.3)
151-
r'\[([A-f0-9:\.]+)\]$',
154+
r'\[([A-f0-9:\.]+)\]\Z',
152155
re.IGNORECASE)
153156
domain_whitelist = ['localhost']
154157

@@ -206,14 +209,14 @@ def __eq__(self, other):
206209

207210
validate_email = EmailValidator()
208211

209-
slug_re = re.compile(r'^[-a-zA-Z0-9_]+$')
212+
slug_re = re.compile(r'^[-a-zA-Z0-9_]+\Z')
210213
validate_slug = RegexValidator(
211214
slug_re,
212215
_("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."),
213216
'invalid'
214217
)
215218

216-
ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$')
219+
ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z')
217220
validate_ipv4_address = RegexValidator(ipv4_re, _('Enter a valid IPv4 address.'), 'invalid')
218221

219222

@@ -254,7 +257,7 @@ def ip_address_validators(protocol, unpack_ipv4):
254257
raise ValueError("The protocol '%s' is unknown. Supported: %s"
255258
% (protocol, list(ip_address_validator_map)))
256259

257-
comma_separated_int_list_re = re.compile('^[\d,]+$')
260+
comma_separated_int_list_re = re.compile('^[\d,]+\Z')
258261
validate_comma_separated_integer_list = RegexValidator(
259262
comma_separated_int_list_re,
260263
_('Enter only digits separated by commas.'),

docs/releases/1.4.21.txt

+26
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,29 @@ As each built-in session backend was fixed separately (rather than a fix in the
2626
core sessions framework), maintainers of third-party session backends should
2727
check whether the same vulnerability is present in their backend and correct
2828
it if so.
29+
30+
Header injection possibility since validators accept newlines in input
31+
======================================================================
32+
33+
Some of Django's built-in validators
34+
(:class:`~django.core.validators.EmailValidator`, most seriously) didn't
35+
prohibit newline characters (due to the usage of ``$`` instead of ``\Z`` in the
36+
regular expressions). If you use values with newlines in HTTP response or email
37+
headers, you can suffer from header injection attacks. Django itself isn't
38+
vulnerable because :class:`~django.http.HttpResponse` and the mail sending
39+
utilities in :mod:`django.core.mail` prohibit newlines in HTTP and SMTP
40+
headers, respectively. While the validators have been fixed in Django, if
41+
you're creating HTTP responses or email messages in other ways, it's a good
42+
idea to ensure that those methods prohibit newlines as well. You might also
43+
want to validate that any existing data in your application doesn't contain
44+
unexpected newlines.
45+
46+
:func:`~django.core.validators.validate_ipv4_address`,
47+
:func:`~django.core.validators.validate_slug`, and
48+
:class:`~django.core.validators.URLValidator` and their usage in the
49+
corresponding form fields ``GenericIPAddresseField``, ``IPAddressField``,
50+
``SlugField``, and ``URLField`` are also affected.
51+
52+
The undocumented, internally unused ``validate_integer()`` function is now
53+
stricter as it validates using a regular expression instead of simply casting
54+
the value using ``int()`` and checking if an exception was raised.

docs/releases/1.7.9.txt

+28
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,34 @@ core sessions framework), maintainers of third-party session backends should
2727
check whether the same vulnerability is present in their backend and correct
2828
it if so.
2929

30+
Header injection possibility since validators accept newlines in input
31+
======================================================================
32+
33+
Some of Django's built-in validators
34+
(:class:`~django.core.validators.EmailValidator`, most seriously) didn't
35+
prohibit newline characters (due to the usage of ``$`` instead of ``\Z`` in the
36+
regular expressions). If you use values with newlines in HTTP response or email
37+
headers, you can suffer from header injection attacks. Django itself isn't
38+
vulnerable because :class:`~django.http.HttpResponse` and the mail sending
39+
utilities in :mod:`django.core.mail` prohibit newlines in HTTP and SMTP
40+
headers, respectively. While the validators have been fixed in Django, if
41+
you're creating HTTP responses or email messages in other ways, it's a good
42+
idea to ensure that those methods prohibit newlines as well. You might also
43+
want to validate that any existing data in your application doesn't contain
44+
unexpected newlines.
45+
46+
:func:`~django.core.validators.validate_ipv4_address`,
47+
:func:`~django.core.validators.validate_slug`, and
48+
:class:`~django.core.validators.URLValidator` are also affected, however, as
49+
of Django 1.6 the ``GenericIPAddresseField``, ``IPAddressField``, ``SlugField``,
50+
and ``URLField`` form fields which use these validators all strip the input, so
51+
the possibility of newlines entering your data only exists if you are using
52+
these validators outside of the form fields.
53+
54+
The undocumented, internally unused ``validate_integer()`` function is now
55+
stricter as it validates using a regular expression instead of simply casting
56+
the value using ``int()`` and checking if an exception was raised.
57+
3058
Bugfixes
3159
========
3260

docs/releases/1.8.3.txt

+28
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,34 @@ core sessions framework), maintainers of third-party session backends should
3232
check whether the same vulnerability is present in their backend and correct
3333
it if so.
3434

35+
Header injection possibility since validators accept newlines in input
36+
======================================================================
37+
38+
Some of Django's built-in validators
39+
(:class:`~django.core.validators.EmailValidator`, most seriously) didn't
40+
prohibit newline characters (due to the usage of ``$`` instead of ``\Z`` in the
41+
regular expressions). If you use values with newlines in HTTP response or email
42+
headers, you can suffer from header injection attacks. Django itself isn't
43+
vulnerable because :class:`~django.http.HttpResponse` and the mail sending
44+
utilities in :mod:`django.core.mail` prohibit newlines in HTTP and SMTP
45+
headers, respectively. While the validators have been fixed in Django, if
46+
you're creating HTTP responses or email messages in other ways, it's a good
47+
idea to ensure that those methods prohibit newlines as well. You might also
48+
want to validate that any existing data in your application doesn't contain
49+
unexpected newlines.
50+
51+
:func:`~django.core.validators.validate_ipv4_address`,
52+
:func:`~django.core.validators.validate_slug`, and
53+
:class:`~django.core.validators.URLValidator` are also affected, however, as
54+
of Django 1.6 the ``GenericIPAddresseField``, ``IPAddressField``, ``SlugField``,
55+
and ``URLField`` form fields which use these validators all strip the input, so
56+
the possibility of newlines entering your data only exists if you are using
57+
these validators outside of the form fields.
58+
59+
The undocumented, internally unused ``validate_integer()`` function is now
60+
stricter as it validates using a regular expression instead of simply casting
61+
the value using ``int()`` and checking if an exception was raised.
62+
3563
Bugfixes
3664
========
3765

tests/validators/tests.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
(validate_integer, '42', None),
2929
(validate_integer, '-42', None),
3030
(validate_integer, -42, None),
31-
(validate_integer, -42.5, None),
3231

32+
(validate_integer, -42.5, ValidationError),
3333
(validate_integer, None, ValidationError),
3434
(validate_integer, 'a', ValidationError),
35+
(validate_integer, '\n42', ValidationError),
36+
(validate_integer, '42\n', ValidationError),
3537

3638
(validate_email, 'email@here.com', None),
3739
(validate_email, 'weirder-email@here.and.there.com', None),
@@ -72,6 +74,11 @@
7274
# Max length of domain name in email is 249 (see validator for calculation)
7375
(validate_email, 'a@%s.us' % ('a' * 249), None),
7476
(validate_email, 'a@%s.us' % ('a' * 250), ValidationError),
77+
# Trailing newlines in username or domain not allowed
78+
(validate_email, 'a@b.com\n', ValidationError),
79+
(validate_email, 'a\n@b.com', ValidationError),
80+
(validate_email, '"test@test"\n@example.com', ValidationError),
81+
(validate_email, 'a@[127.0.0.1]\n', ValidationError),
7582

7683
(validate_slug, 'slug-ok', None),
7784
(validate_slug, 'longer-slug-still-ok', None),
@@ -84,6 +91,7 @@
8491
(validate_slug, 'some@mail.com', ValidationError),
8592
(validate_slug, '你好', ValidationError),
8693
(validate_slug, '\n', ValidationError),
94+
(validate_slug, 'trailing-newline\n', ValidationError),
8795

8896
(validate_ipv4_address, '1.1.1.1', None),
8997
(validate_ipv4_address, '255.0.0.0', None),
@@ -93,6 +101,7 @@
93101
(validate_ipv4_address, '25.1.1.', ValidationError),
94102
(validate_ipv4_address, '25,1,1,1', ValidationError),
95103
(validate_ipv4_address, '25.1 .1.1', ValidationError),
104+
(validate_ipv4_address, '1.1.1.1\n', ValidationError),
96105

97106
# validate_ipv6_address uses django.utils.ipv6, which
98107
# is tested in much greater detail in its own testcase
@@ -126,6 +135,7 @@
126135
(validate_comma_separated_integer_list, '', ValidationError),
127136
(validate_comma_separated_integer_list, 'a,b,c', ValidationError),
128137
(validate_comma_separated_integer_list, '1, 2, 3', ValidationError),
138+
(validate_comma_separated_integer_list, '1,2,3\n', ValidationError),
129139

130140
(MaxValueValidator(10), 10, None),
131141
(MaxValueValidator(10), -10, None),
@@ -159,6 +169,9 @@
159169
(URLValidator(EXTENDED_SCHEMES), 'git://example.com/', None),
160170

161171
(URLValidator(EXTENDED_SCHEMES), 'git://-invalid.com', ValidationError),
172+
# Trailing newlines not accepted
173+
(URLValidator(), 'http://www.djangoproject.com/\n', ValidationError),
174+
(URLValidator(), 'http://[::ffff:192.9.5.5]\n', ValidationError),
162175

163176
(BaseValidator(True), True, None),
164177
(BaseValidator(True), False, ValidationError),

0 commit comments

Comments
 (0)