From 11e67be049665d247b807e066022dfb5a12d94c1 Mon Sep 17 00:00:00 2001
From: Tim Graham <timograham@gmail.com>
Date: Fri, 12 Jun 2015 13:49:31 -0400
Subject: [PATCH 3/3] [1.6.x] Prevented newlines from being accepted in some
 validators. (bnc#937523, CVE-2015-5144)

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 django.http.HttpResponse and the mail sending utilities in 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.

django.core.validators.validate_ipv4_address(), django.core.validators.validate_slug(), and 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.

This is a security fix; disclosure to follow shortly.

Thanks to Sjoerd Job Postmus for the report and draft patch.

Conflicts:
	django/core/validators.py
	docs/releases/1.4.21.txt
	docs/releases/1.7.9.txt
	tests/validators/tests.py
---
 django/core/validators.py | 27 +++++++++++++++------------
 tests/validators/tests.py | 21 +++++++++++++++++++--
 2 files changed, 34 insertions(+), 14 deletions(-)

diff --git a/django/core/validators.py b/django/core/validators.py
index 378da27..0b5ea25 100644
--- a/django/core/validators.py
+++ b/django/core/validators.py
@@ -47,7 +47,7 @@ class URLValidator(RegexValidator):
         r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|'  # ...or ipv4
         r'\[?[A-F0-9]*:[A-F0-9:]+\]?)'  # ...or ipv6
         r'(?::\d+)?'  # optional port
-        r'(?:/?|[/?]\S+)$', re.IGNORECASE)
+        r'(?:/?|[/?]\S+)\Z', re.IGNORECASE)
     message = _('Enter a valid URL.')
 
     def __call__(self, value):
@@ -69,25 +69,28 @@ class URLValidator(RegexValidator):
         else:
             url = value
 
+integer_validator = RegexValidator(
+    re.compile('^-?\d+\Z'),
+    message=_('Enter a valid integer.'),
+    code='invalid',
+)
+
 
 def validate_integer(value):
-    try:
-        int(value)
-    except (ValueError, TypeError):
-        raise ValidationError(_('Enter a valid integer.'), code='invalid')
+    return integer_validator(value)
 
 
 class EmailValidator(object):
     message = _('Enter a valid email address.')
     code = 'invalid'
     user_regex = re.compile(
-        r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$"  # dot-atom
-        r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"$)', # quoted-string
+        r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z"  # dot-atom
+        r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)',  # quoted-string
         re.IGNORECASE)
     domain_regex = re.compile(
-        r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,}(?<!-))$'
+        r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,}(?<!-))\Z'
         # 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}\]$',
+        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)
     domain_whitelist = ['localhost']
 
@@ -125,10 +128,10 @@ class EmailValidator(object):
 
 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(slug_re, _("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, _('Enter a valid IPv4 address.'), 'invalid')
 
 
@@ -169,7 +172,7 @@ def ip_address_validators(protocol, unpack_ipv4):
         raise ValueError("The protocol '%s' is unknown. Supported: %s"
                          % (protocol, list(ip_address_validator_map)))
 
-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, _('Enter only digits separated by commas.'), 'invalid')
 
 
diff --git a/tests/validators/tests.py b/tests/validators/tests.py
index 5315729..57b9ad1 100644
--- a/tests/validators/tests.py
+++ b/tests/validators/tests.py
@@ -18,10 +18,12 @@ TEST_DATA = (
     (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, 'a', ValidationError),
+    (validate_integer, '\n42', ValidationError),
+    (validate_integer, '42\n', ValidationError),
 
     (validate_email, 'email@here.com', None),
     (validate_email, 'weirder-email@here.and.there.com', None),
@@ -51,6 +53,12 @@ TEST_DATA = (
     # Quoted-string format (CR not allowed)
     (validate_email, '"\\\011"@here.com', None),
     (validate_email, '"\\\012"@here.com', ValidationError),
+    (validate_email, 'trailingdot@shouldfail.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, 'longer-slug-still-ok', None),
@@ -63,6 +71,7 @@ TEST_DATA = (
     (validate_slug, 'some@mail.com', ValidationError),
     (validate_slug, '你好', ValidationError),
     (validate_slug, '\n', ValidationError),
+    (validate_slug, 'trailing-newline\n', ValidationError),
 
     (validate_ipv4_address, '1.1.1.1', None),
     (validate_ipv4_address, '255.0.0.0', None),
@@ -72,9 +81,10 @@ TEST_DATA = (
     (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, '1.1.1.1\n', ValidationError),
 
     # 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 its own testcase
     (validate_ipv6_address, 'fe80::1', None),
     (validate_ipv6_address, '::1', None),
     (validate_ipv6_address, '1:2:3:4:5:6:7:8', None),
@@ -105,6 +115,7 @@ TEST_DATA = (
     (validate_comma_separated_integer_list, '', 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\n', ValidationError),
 
     (MaxValueValidator(10), 10, None),
     (MaxValueValidator(10), -10, None),
@@ -157,6 +168,12 @@ TEST_DATA = (
     (URLValidator(), 'http://invalid.com-', ValidationError),
     (URLValidator(), 'http://inv-.alid-.com', ValidationError),
     (URLValidator(), 'http://inv-.-alid.com', ValidationError),
+    (URLValidator(), 'file://localhost/path', ValidationError),
+    (URLValidator(), 'git://example.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), False, ValidationError),
-- 
2.1.4

