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.
This commit is contained in:
Tim Graham 2015-06-12 13:49:31 -04:00
parent df049ed77a
commit 014247ad19
5 changed files with 111 additions and 13 deletions

View File

@ -83,7 +83,7 @@ class URLValidator(RegexValidator):
r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')' r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')'
r'(?::\d{2,5})?' # port r'(?::\d{2,5})?' # port
r'(?:[/?#][^\s]*)?' # resource path r'(?:[/?#][^\s]*)?' # resource path
r'$', re.IGNORECASE) r'\Z', re.IGNORECASE)
message = _('Enter a valid URL.') message = _('Enter a valid URL.')
schemes = ['http', 'https', 'ftp', 'ftps'] schemes = ['http', 'https', 'ftp', 'ftps']
@ -125,12 +125,15 @@ class URLValidator(RegexValidator):
raise ValidationError(self.message, code=self.code) raise ValidationError(self.message, code=self.code)
url = value url = value
integer_validator = RegexValidator(
re.compile('^-?\d+\Z'),
message=_('Enter a valid integer.'),
code='invalid',
)
def validate_integer(value): def validate_integer(value):
try: return integer_validator(value)
int(value)
except (ValueError, TypeError):
raise ValidationError(_('Enter a valid integer.'), code='invalid')
@deconstructible @deconstructible
@ -138,16 +141,16 @@ class EmailValidator(object):
message = _('Enter a valid email address.') message = _('Enter a valid email address.')
code = 'invalid' code = 'invalid'
user_regex = re.compile( user_regex = re.compile(
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$" # dot-atom r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z" # dot-atom
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"$)', # quoted-string r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)', # quoted-string
re.IGNORECASE) re.IGNORECASE)
domain_regex = re.compile( domain_regex = re.compile(
# max length for domain name labels is 63 characters per RFC 1034 # max length for domain name labels is 63 characters per RFC 1034
r'((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(?<!-))$', r'((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(?<!-))\Z',
re.IGNORECASE) re.IGNORECASE)
literal_regex = re.compile( literal_regex = re.compile(
# literal form, ipv4 or ipv6 address (SMTP 4.1.3) # literal form, ipv4 or ipv6 address (SMTP 4.1.3)
r'\[([A-f0-9:\.]+)\]$', r'\[([A-f0-9:\.]+)\]\Z',
re.IGNORECASE) re.IGNORECASE)
domain_whitelist = ['localhost'] domain_whitelist = ['localhost']
@ -205,14 +208,14 @@ class EmailValidator(object):
validate_email = EmailValidator() validate_email = EmailValidator()
slug_re = re.compile(r'^[-a-zA-Z0-9_]+$') slug_re = re.compile(r'^[-a-zA-Z0-9_]+\Z')
validate_slug = RegexValidator( validate_slug = RegexValidator(
slug_re, slug_re,
_("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."), _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."),
'invalid' 'invalid'
) )
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}$') 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')
validate_ipv4_address = RegexValidator(ipv4_re, _('Enter a valid IPv4 address.'), 'invalid') validate_ipv4_address = RegexValidator(ipv4_re, _('Enter a valid IPv4 address.'), 'invalid')
@ -255,7 +258,7 @@ def ip_address_validators(protocol, unpack_ipv4):
def int_list_validator(sep=',', message=None, code='invalid'): def int_list_validator(sep=',', message=None, code='invalid'):
regexp = re.compile('^\d+(?:%s\d+)*$' % re.escape(sep)) regexp = re.compile('^\d+(?:%s\d+)*\Z' % re.escape(sep))
return RegexValidator(regexp, message=message, code=code) return RegexValidator(regexp, message=message, code=code)

View File

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

View File

@ -27,6 +27,34 @@ core sessions framework), maintainers of third-party session backends should
check whether the same vulnerability is present in their backend and correct check whether the same vulnerability is present in their backend and correct
it if so. it if so.
Header injection possibility since validators accept newlines in input
======================================================================
Some of Django's built-in validators
(:class:`~django.core.validators.EmailValidator`, most seriously) didn't
prohibit newline characters (due to the usage of ``$`` instead of ``\Z`` in the
regular expressions). If you use values with newlines in HTTP response or email
headers, you can suffer from header injection attacks. Django itself isn't
vulnerable because :class:`~django.http.HttpResponse` and the mail sending
utilities in :mod:`django.core.mail` prohibit newlines in HTTP and SMTP
headers, respectively. While the validators have been fixed in Django, if
you're creating HTTP responses or email messages in other ways, it's a good
idea to ensure that those methods prohibit newlines as well. You might also
want to validate that any existing data in your application doesn't contain
unexpected newlines.
:func:`~django.core.validators.validate_ipv4_address`,
:func:`~django.core.validators.validate_slug`, and
:class:`~django.core.validators.URLValidator` are also affected, however, as
of Django 1.6 the ``GenericIPAddresseField``, ``IPAddressField``, ``SlugField``,
and ``URLField`` form fields which use these validators all strip the input, so
the possibility of newlines entering your data only exists if you are using
these validators outside of the form fields.
The undocumented, internally unused ``validate_integer()`` function is now
stricter as it validates using a regular expression instead of simply casting
the value using ``int()`` and checking if an exception was raised.
Bugfixes Bugfixes
======== ========

View File

@ -32,6 +32,34 @@ core sessions framework), maintainers of third-party session backends should
check whether the same vulnerability is present in their backend and correct check whether the same vulnerability is present in their backend and correct
it if so. it if so.
Header injection possibility since validators accept newlines in input
======================================================================
Some of Django's built-in validators
(:class:`~django.core.validators.EmailValidator`, most seriously) didn't
prohibit newline characters (due to the usage of ``$`` instead of ``\Z`` in the
regular expressions). If you use values with newlines in HTTP response or email
headers, you can suffer from header injection attacks. Django itself isn't
vulnerable because :class:`~django.http.HttpResponse` and the mail sending
utilities in :mod:`django.core.mail` prohibit newlines in HTTP and SMTP
headers, respectively. While the validators have been fixed in Django, if
you're creating HTTP responses or email messages in other ways, it's a good
idea to ensure that those methods prohibit newlines as well. You might also
want to validate that any existing data in your application doesn't contain
unexpected newlines.
:func:`~django.core.validators.validate_ipv4_address`,
:func:`~django.core.validators.validate_slug`, and
:class:`~django.core.validators.URLValidator` are also affected, however, as
of Django 1.6 the ``GenericIPAddresseField``, ``IPAddressField``, ``SlugField``,
and ``URLField`` form fields which use these validators all strip the input, so
the possibility of newlines entering your data only exists if you are using
these validators outside of the form fields.
The undocumented, internally unused ``validate_integer()`` function is now
stricter as it validates using a regular expression instead of simply casting
the value using ``int()`` and checking if an exception was raised.
Bugfixes Bugfixes
======== ========

View File

@ -28,10 +28,12 @@ TEST_DATA = [
(validate_integer, '42', None), (validate_integer, '42', None),
(validate_integer, '-42', None), (validate_integer, '-42', None),
(validate_integer, -42, None), (validate_integer, -42, None),
(validate_integer, -42.5, None),
(validate_integer, -42.5, ValidationError),
(validate_integer, None, ValidationError), (validate_integer, None, ValidationError),
(validate_integer, 'a', ValidationError), (validate_integer, 'a', ValidationError),
(validate_integer, '\n42', ValidationError),
(validate_integer, '42\n', ValidationError),
(validate_email, 'email@here.com', None), (validate_email, 'email@here.com', None),
(validate_email, 'weirder-email@here.and.there.com', None), (validate_email, 'weirder-email@here.and.there.com', None),
@ -77,6 +79,11 @@ TEST_DATA = [
# Max length of domain name labels is 63 characters per RFC 1034. # Max length of domain name labels is 63 characters per RFC 1034.
(validate_email, 'a@%s.us' % ('a' * 63), None), (validate_email, 'a@%s.us' % ('a' * 63), None),
(validate_email, 'a@%s.us' % ('a' * 64), ValidationError), (validate_email, 'a@%s.us' % ('a' * 64), ValidationError),
# Trailing newlines in username or domain not allowed
(validate_email, 'a@b.com\n', ValidationError),
(validate_email, 'a\n@b.com', ValidationError),
(validate_email, '"test@test"\n@example.com', ValidationError),
(validate_email, 'a@[127.0.0.1]\n', ValidationError),
(validate_slug, 'slug-ok', None), (validate_slug, 'slug-ok', None),
(validate_slug, 'longer-slug-still-ok', None), (validate_slug, 'longer-slug-still-ok', None),
@ -89,6 +96,7 @@ TEST_DATA = [
(validate_slug, 'some@mail.com', ValidationError), (validate_slug, 'some@mail.com', ValidationError),
(validate_slug, '你好', ValidationError), (validate_slug, '你好', ValidationError),
(validate_slug, '\n', ValidationError), (validate_slug, '\n', ValidationError),
(validate_slug, 'trailing-newline\n', ValidationError),
(validate_ipv4_address, '1.1.1.1', None), (validate_ipv4_address, '1.1.1.1', None),
(validate_ipv4_address, '255.0.0.0', None), (validate_ipv4_address, '255.0.0.0', None),
@ -98,6 +106,7 @@ TEST_DATA = [
(validate_ipv4_address, '25.1.1.', ValidationError), (validate_ipv4_address, '25.1.1.', ValidationError),
(validate_ipv4_address, '25,1,1,1', ValidationError), (validate_ipv4_address, '25,1,1,1', ValidationError),
(validate_ipv4_address, '25.1 .1.1', ValidationError), (validate_ipv4_address, '25.1 .1.1', ValidationError),
(validate_ipv4_address, '1.1.1.1\n', ValidationError),
# validate_ipv6_address uses django.utils.ipv6, which # validate_ipv6_address uses django.utils.ipv6, which
# is tested in much greater detail in its own testcase # is tested in much greater detail in its own testcase
@ -142,6 +151,7 @@ TEST_DATA = [
(int_list_validator(sep='.'), '1.2.3', None), (int_list_validator(sep='.'), '1.2.3', None),
(int_list_validator(sep='.'), '1,2,3', ValidationError), (int_list_validator(sep='.'), '1,2,3', ValidationError),
(int_list_validator(sep='.'), '1.2.3\n', ValidationError),
(MaxValueValidator(10), 10, None), (MaxValueValidator(10), 10, None),
(MaxValueValidator(10), -10, None), (MaxValueValidator(10), -10, None),
@ -175,6 +185,9 @@ TEST_DATA = [
(URLValidator(EXTENDED_SCHEMES), 'git://example.com/', None), (URLValidator(EXTENDED_SCHEMES), 'git://example.com/', None),
(URLValidator(EXTENDED_SCHEMES), 'git://-invalid.com', ValidationError), (URLValidator(EXTENDED_SCHEMES), 'git://-invalid.com', ValidationError),
# Trailing newlines not accepted
(URLValidator(), 'http://www.djangoproject.com/\n', ValidationError),
(URLValidator(), 'http://[::ffff:192.9.5.5]\n', ValidationError),
(BaseValidator(True), True, None), (BaseValidator(True), True, None),
(BaseValidator(True), False, ValidationError), (BaseValidator(True), False, ValidationError),