[1.4.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.
This commit is contained in:
Tim Graham 2015-06-12 13:49:31 -04:00
parent 2e47f3e401
commit 1ba1cdce7d
3 changed files with 56 additions and 12 deletions

View File

@ -50,7 +50,7 @@ class URLValidator(RegexValidator):
r'localhost|' #localhost... r'localhost|' #localhost...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
r'(?::\d+)?' # optional port r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE) r'(?:/?|[/?]\S+)\Z', re.IGNORECASE)
def __init__(self, verify_exists=False, def __init__(self, verify_exists=False,
validator_user_agent=URL_VALIDATOR_USER_AGENT): validator_user_agent=URL_VALIDATOR_USER_AGENT):
@ -133,11 +133,16 @@ class URLValidator(RegexValidator):
raise broken_error raise broken_error
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('')
class EmailValidator(RegexValidator): class EmailValidator(RegexValidator):
@ -160,14 +165,14 @@ email_re = re.compile(
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom
# quoted-string, see also http://tools.ietf.org/html/rfc2822#section-3.2.5 # quoted-string, see also http://tools.ietf.org/html/rfc2822#section-3.2.5
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"'
r')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$)' # domain r')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?\Z)' # domain
r'|\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE) # literal form, ipv4 address (SMTP 4.1.3) 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', re.IGNORECASE) # literal form, ipv4 address (SMTP 4.1.3)
validate_email = EmailValidator(email_re, _(u'Enter a valid e-mail address.'), 'invalid') validate_email = EmailValidator(email_re, _(u'Enter a valid e-mail address.'), 'invalid')
slug_re = re.compile(r'^[-\w]+$') slug_re = re.compile(r'^[-\w]+\Z')
validate_slug = RegexValidator(slug_re, _(u"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."), 'invalid') validate_slug = RegexValidator(slug_re, _(u"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."), '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, _(u'Enter a valid IPv4 address.'), 'invalid') validate_ipv4_address = RegexValidator(ipv4_re, _(u'Enter a valid IPv4 address.'), 'invalid')
def validate_ipv6_address(value): def validate_ipv6_address(value):
@ -205,7 +210,7 @@ def ip_address_validators(protocol, unpack_ipv4):
raise ValueError("The protocol '%s' is unknown. Supported: %s" raise ValueError("The protocol '%s' is unknown. Supported: %s"
% (protocol, ip_address_validator_map.keys())) % (protocol, ip_address_validator_map.keys()))
comma_separated_int_list_re = re.compile('^[\d,]+$') comma_separated_int_list_re = re.compile('^[\d,]+\Z')
validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_re, _(u'Enter only digits separated by commas.'), 'invalid') validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_re, _(u'Enter only digits separated by commas.'), 'invalid')
@ -249,4 +254,3 @@ class MaxLengthValidator(BaseValidator):
clean = lambda self, x: len(x) clean = lambda self, x: len(x)
message = _(u'Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).') message = _(u'Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).')
code = 'max_length' code = 'max_length'

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
(``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

@ -11,14 +11,17 @@ from django.utils.unittest import TestCase
NOW = datetime.now() NOW = datetime.now()
TEST_DATA = ( TEST_DATA = (
# (validator, value, expected),
# (validator, value, expected), # (validator, value, expected),
(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),
@ -33,6 +36,11 @@ TEST_DATA = (
# Quoted-string format (CR not allowed) # Quoted-string format (CR not allowed)
(validate_email, '"\\\011"@here.com', None), (validate_email, '"\\\011"@here.com', None),
(validate_email, '"\\\012"@here.com', ValidationError), (validate_email, '"\\\012"@here.com', 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),
@ -45,6 +53,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),
@ -54,6 +63,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 it's own testcase # is tested in much greater detail in it's own testcase
@ -87,6 +97,7 @@ TEST_DATA = (
(validate_comma_separated_integer_list, '', ValidationError), (validate_comma_separated_integer_list, '', ValidationError),
(validate_comma_separated_integer_list, 'a,b,c', ValidationError), (validate_comma_separated_integer_list, 'a,b,c', ValidationError),
(validate_comma_separated_integer_list, '1, 2, 3', ValidationError), (validate_comma_separated_integer_list, '1, 2, 3', ValidationError),
(validate_comma_separated_integer_list, '1,2,3\n', ValidationError),
(MaxValueValidator(10), 10, None), (MaxValueValidator(10), 10, None),
(MaxValueValidator(10), -10, None), (MaxValueValidator(10), -10, None),
@ -138,6 +149,9 @@ TEST_DATA = (
(URLValidator(), 'http://-invalid.com', ValidationError), (URLValidator(), 'http://-invalid.com', ValidationError),
(URLValidator(), 'http://inv-.alid-.com', ValidationError), (URLValidator(), 'http://inv-.alid-.com', ValidationError),
(URLValidator(), 'http://inv-.-alid.com', ValidationError), (URLValidator(), 'http://inv-.-alid.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),