From 1ba1cdce7d58e6740fe51955d945b56ae51d072a Mon Sep 17 00:00:00 2001
From: Tim Graham <timograham@gmail.com>
Date: Fri, 12 Jun 2015 13:49:31 -0400
Subject: [PATCH] [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.
---
 django/core/validators.py            | 26 +++++++++++++++-----------
 docs/releases/1.4.21.txt             | 26 ++++++++++++++++++++++++++
 tests/modeltests/validators/tests.py | 16 +++++++++++++++-
 3 files changed, 56 insertions(+), 12 deletions(-)

--- a/django/core/validators.py
+++ b/django/core/validators.py
@@ -45,7 +45,7 @@ class URLValidator(RegexValidator):
         r'localhost|' #localhost...
         r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
         r'(?::\d+)?' # optional port
-        r'(?:/?|[/?]\S+)$', re.IGNORECASE)
+        r'(?:/?|[/?]\S+)\Z', re.IGNORECASE)
 
     def __init__(self, verify_exists=False, validator_user_agent=URL_VALIDATOR_USER_AGENT):
         super(URLValidator, self).__init__()
@@ -89,11 +89,16 @@ class URLValidator(RegexValidator):
                 raise ValidationError(_(u'This URL appears to be a broken link.'), code='invalid_link')
 
 
+integer_validator = RegexValidator(
+    re.compile('^-?\d+\Z'),
+    message=_('Enter a valid integer.'),
+    code='invalid',
+)
+
+
 def validate_integer(value):
-    try:
-        int(value)
-    except (ValueError, TypeError), e:
-        raise ValidationError('')
+    return integer_validator(value)
+
 
 class EmailValidator(RegexValidator):
 
@@ -116,16 +121,16 @@ class EmailValidator(RegexValidator):
 email_re = 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')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$', re.IGNORECASE)  # domain
+    r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$\Z', re.IGNORECASE)  # domain
 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')
 
-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')
 
-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')
 
 
--- a/tests/modeltests/validators/tests.py
+++ b/tests/modeltests/validators/tests.py
@@ -10,13 +10,16 @@ NOW = datetime.now()
 
 TEST_DATA = (
     # (validator, value, expected),
+    # (validator, value, expected),
     (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),
@@ -26,6 +29,10 @@ TEST_DATA = (
     (validate_email, 'abc', ValidationError),
     (validate_email, 'a @x.cz', ValidationError),
     (validate_email, 'something@@somewhere.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_slug, 'slug-ok', None),
     (validate_slug, 'longer-slug-still-ok', None),
@@ -38,6 +45,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),
@@ -47,6 +55,7 @@ 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_comma_separated_integer_list, '1', None),
     (validate_comma_separated_integer_list, '1,2,3', None),
@@ -55,6 +64,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),
@@ -106,6 +116,9 @@ TEST_DATA = (
     (URLValidator(), 'http://-invalid.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), False, ValidationError),
