Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1a76dbe

Browse files
committed
[1.3.X] Altered the behavior of URLField to avoid a potential DOS vector, and to avoid potential leakage of local filesystem data. A security announcement will be made shortly.
Backport of r16760 from trunk. git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.3.X@16763 bcc190cf-cafb-0310-a4f2-bffc1f526a37
1 parent fbe2eea commit 1a76dbe

File tree

10 files changed

+87
-57
lines changed

10 files changed

+87
-57
lines changed

django/core/validators.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import platform
12
import re
23
import urllib2
34
import urlparse
@@ -39,10 +40,6 @@ def __call__(self, value):
3940
if not self.regex.search(smart_unicode(value)):
4041
raise ValidationError(self.message, code=self.code)
4142

42-
class HeadRequest(urllib2.Request):
43-
def get_method(self):
44-
return "HEAD"
45-
4643
class URLValidator(RegexValidator):
4744
regex = re.compile(
4845
r'^(?:http|ftp)s?://' # http:// or https://
@@ -52,7 +49,8 @@ class URLValidator(RegexValidator):
5249
r'(?::\d+)?' # optional port
5350
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
5451

55-
def __init__(self, verify_exists=False, validator_user_agent=URL_VALIDATOR_USER_AGENT):
52+
def __init__(self, verify_exists=False,
53+
validator_user_agent=URL_VALIDATOR_USER_AGENT):
5654
super(URLValidator, self).__init__()
5755
self.verify_exists = verify_exists
5856
self.user_agent = validator_user_agent
@@ -76,6 +74,7 @@ def __call__(self, value):
7674
else:
7775
url = value
7876

77+
#This is deprecated and will be removed in a future release.
7978
if self.verify_exists:
8079
headers = {
8180
"Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
@@ -88,21 +87,36 @@ def __call__(self, value):
8887
broken_error = ValidationError(
8988
_(u'This URL appears to be a broken link.'), code='invalid_link')
9089
try:
91-
req = HeadRequest(url, None, headers)
92-
u = urllib2.urlopen(req)
90+
req = urllib2.Request(url, None, headers)
91+
req.get_method = lambda: 'HEAD'
92+
#Create an opener that does not support local file access
93+
opener = urllib2.OpenerDirector()
94+
95+
#Don't follow redirects, but don't treat them as errors either
96+
error_nop = lambda *args, **kwargs: True
97+
http_error_processor = urllib2.HTTPErrorProcessor()
98+
http_error_processor.http_error_301 = error_nop
99+
http_error_processor.http_error_302 = error_nop
100+
http_error_processor.http_error_307 = error_nop
101+
102+
handlers = [urllib2.UnknownHandler(),
103+
urllib2.HTTPHandler(),
104+
urllib2.HTTPDefaultErrorHandler(),
105+
urllib2.FTPHandler(),
106+
http_error_processor]
107+
try:
108+
import ssl
109+
handlers.append(urllib2.HTTPSHandler())
110+
except:
111+
#Python isn't compiled with SSL support
112+
pass
113+
map(opener.add_handler, handlers)
114+
if platform.python_version_tuple() >= (2, 6):
115+
opener.open(req, timeout=10)
116+
else:
117+
opener.open(req)
93118
except ValueError:
94119
raise ValidationError(_(u'Enter a valid URL.'), code='invalid')
95-
except urllib2.HTTPError, e:
96-
if e.code in (405, 501):
97-
# Try a GET request (HEAD refused)
98-
# See also: http://www.w3.org/Protocols/rfc2616/rfc2616.html
99-
try:
100-
req = urllib2.Request(url, None, headers)
101-
u = urllib2.urlopen(req)
102-
except:
103-
raise broken_error
104-
else:
105-
raise broken_error
106120
except: # urllib2.URLError, httplib.InvalidURL, etc.
107121
raise broken_error
108122

django/db/models/fields/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1119,7 +1119,7 @@ def formfield(self, **kwargs):
11191119
class URLField(CharField):
11201120
description = _("URL")
11211121

1122-
def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs):
1122+
def __init__(self, verbose_name=None, name=None, verify_exists=False, **kwargs):
11231123
kwargs['max_length'] = kwargs.get('max_length', 200)
11241124
CharField.__init__(self, verbose_name, name, **kwargs)
11251125
self.validators.append(validators.URLValidator(verify_exists=verify_exists))

docs/internals/deprecation.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ their deprecation, as per the :ref:`Django deprecation policy
108108
beyond that of a simple ``TextField`` since the removal of oldforms.
109109
All uses of ``XMLField`` can be replaced with ``TextField``.
110110

111+
* ``django.db.models.fields.URLField.verify_exists`` has been
112+
deprecated due to intractable security and performance
113+
issues. Validation behavior has been removed in 1.4, and the
114+
argument will be removed in 1.5.
115+
116+
111117
* 1.5
112118
* The ``mod_python`` request handler has been deprecated since the 1.3
113119
release. The ``mod_wsgi`` handler should be used instead.

docs/ref/forms/fields.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,11 @@ Takes the following optional arguments:
756756
If ``True``, the validator will attempt to load the given URL, raising
757757
``ValidationError`` if the page gives a 404. Defaults to ``False``.
758758

759+
.. deprecated:: 1.3.1
760+
761+
``verify_exists`` was deprecated for security reasons and will be
762+
removed in 1.4. This deprecation also removes ``validator_user_agent``.
763+
759764
.. attribute:: URLField.validator_user_agent
760765

761766
String used as the user-agent used when checking for a URL's existence.

docs/ref/models/fields.txt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -831,14 +831,21 @@ shortcuts.
831831
``URLField``
832832
------------
833833

834-
.. class:: URLField([verify_exists=True, max_length=200, **options])
834+
.. class:: URLField([verify_exists=False, max_length=200, **options])
835835

836836
A :class:`CharField` for a URL. Has one extra optional argument:
837837

838+
.. deprecated:: 1.3.1
839+
840+
``verify_exists`` is deprecated for security reasons as of 1.3.1
841+
and will be removed in 1.4. Prior to 1.3.1, the default value was
842+
``True``.
843+
838844
.. attribute:: URLField.verify_exists
839845

840-
If ``True`` (the default), the URL given will be checked for existence
841-
(i.e., the URL actually loads and doesn't give a 404 response).
846+
If ``True``, the URL given will be checked for existence (i.e.,
847+
the URL actually loads and doesn't give a 404 response) using a
848+
``HEAD`` request. Redirects are allowed, but will not be followed.
842849

843850
Note that when you're using the single-threaded development server,
844851
validating a URL being served by the same server will hang. This should not

docs/ref/settings.txt

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1892,16 +1892,6 @@ to ensure your processes are running in the correct environment.
18921892

18931893
.. _See available choices: http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
18941894

1895-
.. setting:: URL_VALIDATOR_USER_AGENT
1896-
1897-
URL_VALIDATOR_USER_AGENT
1898-
------------------------
1899-
1900-
Default: ``Django/<version> (http://www.djangoproject.com/)``
1901-
1902-
The string to use as the ``User-Agent`` header when checking to see if URLs
1903-
exist (see the ``verify_exists`` option on :class:`~django.db.models.URLField`).
1904-
19051895
.. setting:: USE_ETAGS
19061896

19071897
USE_ETAGS
@@ -2095,3 +2085,19 @@ TEST_DATABASE_NAME
20952085
This setting has been replaced by :setting:`TEST_NAME` in
20962086
:setting:`DATABASES`.
20972087

2088+
2089+
2090+
URL_VALIDATOR_USER_AGENT
2091+
------------------------
2092+
2093+
.. deprecated:: 1.3.1
2094+
This setting has been removed due to intractable performance and
2095+
security problems.
2096+
2097+
Default: ``Django/<version> (http://www.djangoproject.com/)``
2098+
2099+
The string to use as the ``User-Agent`` header when checking to see if
2100+
URLs exist (see the ``verify_exists`` option on
2101+
:class:`~django.db.models.URLField`). This setting was deprecated in
2102+
1.3.1 along with ``verify_exists`` and will be removed in 1.4.
2103+

tests/modeltests/validation/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from django.utils import unittest
1+
from django.test import TestCase
22

33
from django.core.exceptions import ValidationError
44

5-
class ValidationTestCase(unittest.TestCase):
5+
class ValidationTestCase(TestCase):
66
def assertFailsValidation(self, clean, failed_fields):
77
self.assertRaises(ValidationError, clean)
88
try:

tests/modeltests/validation/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class ModelToValidate(models.Model):
1515
parent = models.ForeignKey('self', blank=True, null=True, limit_choices_to={'number': 10})
1616
email = models.EmailField(blank=True)
1717
url = models.URLField(blank=True)
18+
url_verify = models.URLField(blank=True, verify_exists=True)
1819
f_with_custom_validator = models.IntegerField(blank=True, null=True, validators=[validate_answer_to_universe])
1920

2021
def clean(self):

tests/modeltests/validation/tests.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,25 +53,22 @@ def test_wrong_url_value_raises_error(self):
5353
mtv = ModelToValidate(number=10, name='Some Name', url='not a url')
5454
self.assertFieldFailsValidationWithMessage(mtv.full_clean, 'url', [u'Enter a valid value.'])
5555

56+
#The tests below which use url_verify are deprecated
5657
def test_correct_url_but_nonexisting_gives_404(self):
57-
mtv = ModelToValidate(number=10, name='Some Name', url='http://google.com/we-love-microsoft.html')
58-
self.assertFieldFailsValidationWithMessage(mtv.full_clean, 'url', [u'This URL appears to be a broken link.'])
58+
mtv = ModelToValidate(number=10, name='Some Name', url_verify='http://qa-dev.w3.org/link-testsuite/http.php?code=404')
59+
self.assertFieldFailsValidationWithMessage(mtv.full_clean, 'url_verify', [u'This URL appears to be a broken link.'])
5960

6061
def test_correct_url_value_passes(self):
61-
mtv = ModelToValidate(number=10, name='Some Name', url='http://www.example.com/')
62+
mtv = ModelToValidate(number=10, name='Some Name', url_verify='http://www.google.com/')
6263
self.assertEqual(None, mtv.full_clean()) # This will fail if there's no Internet connection
6364

64-
def test_correct_https_url_but_nonexisting(self):
65-
mtv = ModelToValidate(number=10, name='Some Name', url='https://www.example.com/')
66-
self.assertFieldFailsValidationWithMessage(mtv.full_clean, 'url', [u'This URL appears to be a broken link.'])
67-
68-
def test_correct_ftp_url_but_nonexisting(self):
69-
mtv = ModelToValidate(number=10, name='Some Name', url='ftp://ftp.google.com/we-love-microsoft.html')
70-
self.assertFieldFailsValidationWithMessage(mtv.full_clean, 'url', [u'This URL appears to be a broken link.'])
65+
def test_correct_url_with_redirect(self):
66+
mtv = ModelToValidate(number=10, name='Some Name', url_verify='http://qa-dev.w3.org/link-testsuite/http.php?code=301') #example.com is a redirect to iana.org now
67+
self.assertEqual(None, mtv.full_clean()) # This will fail if there's no Internet connection
7168

72-
def test_correct_ftps_url_but_nonexisting(self):
73-
mtv = ModelToValidate(number=10, name='Some Name', url='ftps://ftp.google.com/we-love-microsoft.html')
74-
self.assertFieldFailsValidationWithMessage(mtv.full_clean, 'url', [u'This URL appears to be a broken link.'])
69+
def test_correct_https_url_but_nonexisting(self):
70+
mtv = ModelToValidate(number=10, name='Some Name', url_verify='https://www.example.com/')
71+
self.assertFieldFailsValidationWithMessage(mtv.full_clean, 'url_verify', [u'This URL appears to be a broken link.'])
7572

7673
def test_text_greater_that_charfields_max_length_raises_erros(self):
7774
mtv = ModelToValidate(number=10, name='Some Name'*100)

tests/regressiontests/forms/tests/fields.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ def test_urlfield_3(self):
567567
f.clean('http://www.broken.djangoproject.com') # bad domain
568568
except ValidationError, e:
569569
self.assertEqual("[u'This URL appears to be a broken link.']", str(e))
570-
self.assertRaises(ValidationError, f.clean, 'http://google.com/we-love-microsoft.html') # good domain, bad page
570+
self.assertRaises(ValidationError, f.clean, 'http://qa-dev.w3.org/link-testsuite/http.php?code=400') # good domain, bad page
571571
try:
572572
f.clean('http://google.com/we-love-microsoft.html') # good domain, bad page
573573
except ValidationError, e:
@@ -626,16 +626,10 @@ def test_urlfield_9(self):
626626
self.assertEqual("[u'This URL appears to be a broken link.']", str(e))
627627

628628
def test_urlfield_10(self):
629-
# UTF-8 char in path, enclosed by a monkey-patch to make sure
630-
# the encoding is passed to urllib2.urlopen
629+
# UTF-8 in the domain.
631630
f = URLField(verify_exists=True)
632-
try:
633-
_orig_urlopen = urllib2.urlopen
634-
urllib2.urlopen = lambda req: True
635-
url = u'http://t\xfcr.djangoproject.com/'
636-
self.assertEqual(url, f.clean(url))
637-
finally:
638-
urllib2.urlopen = _orig_urlopen
631+
url = u'http://\u03b5\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac.idn.icann.org/\u0391\u03c1\u03c7\u03b9\u03ba\u03ae_\u03c3\u03b5\u03bb\u03af\u03b4\u03b1'
632+
self.assertEqual(url, f.clean(url)) #This will fail without internet.
639633

640634
# BooleanField ################################################################
641635

0 commit comments

Comments
 (0)