diff options
| author | Carl Meyer <carl@oddbird.net> | 2013-02-09 11:32:51 -0700 |
|---|---|---|
| committer | Carl Meyer <carl@oddbird.net> | 2013-02-19 10:37:54 -0700 |
| commit | 9936fdb11d0bbf0bd242f259bfb97bbf849d16f8 (patch) | |
| tree | 43770281aab2cce7da09de070c347235d7d31ebe /django | |
| parent | 57b62a74cb44ac3cf8b1a9d4cf75bfe953208814 (diff) | |
[1.4.x] Added ALLOWED_HOSTS setting for HTTP host header validation.
This is a security fix; disclosure and advisory coming shortly.
Diffstat (limited to 'django')
| -rw-r--r-- | django/conf/global_settings.py | 4 | ||||
| -rw-r--r-- | django/conf/project_template/project_name/settings.py | 4 | ||||
| -rw-r--r-- | django/contrib/auth/tests/views.py | 1 | ||||
| -rw-r--r-- | django/contrib/contenttypes/tests.py | 2 | ||||
| -rw-r--r-- | django/contrib/sites/tests.py | 2 | ||||
| -rw-r--r-- | django/http/__init__.py | 51 | ||||
| -rw-r--r-- | django/test/utils.py | 6 |
7 files changed, 65 insertions, 5 deletions
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index bd85c121b8..026e39b7c9 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -29,6 +29,10 @@ ADMINS = () # * Receive x-headers INTERNAL_IPS = () +# Hosts/domain names that are valid for this site. +# "*" matches anything, ".example.com" matches example.com and all subdomains +ALLOWED_HOSTS = ['*'] + # Local time zone for this installation. All choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all # systems may support all possibilities). When USE_TZ is True, this is diff --git a/django/conf/project_template/project_name/settings.py b/django/conf/project_template/project_name/settings.py index 0eccc4eaf5..5780aca012 100644 --- a/django/conf/project_template/project_name/settings.py +++ b/django/conf/project_template/project_name/settings.py @@ -20,6 +20,10 @@ DATABASES = { } } +# Hosts/domain names that are valid for this site; required if DEBUG is False +# See https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#allowed-hosts +ALLOWED_HOSTS = [] + # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index d295bb8c10..603d380e9d 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -107,6 +107,7 @@ class PasswordResetTest(AuthViewsTestCase): self.assertEqual(len(mail.outbox), 1) self.assertEqual("staffmember@example.com", mail.outbox[0].from_email) + @override_settings(ALLOWED_HOSTS=['adminsite.com']) def test_admin_reset(self): "If the reset view is marked as being for admin, the HTTP_HOST header is used for a domain override." response = self.client.post('/admin_password_reset/', diff --git a/django/contrib/contenttypes/tests.py b/django/contrib/contenttypes/tests.py index 3b7906c812..66226a78b8 100644 --- a/django/contrib/contenttypes/tests.py +++ b/django/contrib/contenttypes/tests.py @@ -9,6 +9,7 @@ from django.contrib.sites.models import Site from django.http import HttpRequest, Http404 from django.test import TestCase from django.utils.encoding import smart_str +from django.test.utils import override_settings class FooWithoutUrl(models.Model): @@ -114,6 +115,7 @@ class ContentTypesTests(TestCase): FooWithUrl: ContentType.objects.get_for_model(FooWithUrl), }) + @override_settings(ALLOWED_HOSTS=['example.com']) def test_shortcut_view(self): """ Check that the shortcut view (used for the admin "view on site" diff --git a/django/contrib/sites/tests.py b/django/contrib/sites/tests.py index 828badb386..1fd52e657e 100644 --- a/django/contrib/sites/tests.py +++ b/django/contrib/sites/tests.py @@ -3,6 +3,7 @@ from django.contrib.sites.models import Site, RequestSite, get_current_site from django.core.exceptions import ObjectDoesNotExist from django.http import HttpRequest from django.test import TestCase +from django.test.utils import override_settings class SitesFrameworkTests(TestCase): @@ -39,6 +40,7 @@ class SitesFrameworkTests(TestCase): site = Site.objects.get_current() self.assertEqual(u"Example site", site.name) + @override_settings(ALLOWED_HOSTS=['example.com']) def test_get_current_site(self): # Test that the correct Site object is returned request = HttpRequest() diff --git a/django/http/__init__.py b/django/http/__init__.py index da993eb8d3..4f5fbe6cf3 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -215,11 +215,12 @@ class HttpRequest(object): if server_port != (self.is_secure() and '443' or '80'): host = '%s:%s' % (host, server_port) - # Disallow potentially poisoned hostnames. - if not host_validation_re.match(host.lower()): - raise SuspiciousOperation('Invalid HTTP_HOST header: %s' % host) - - return host + allowed_hosts = ['*'] if settings.DEBUG else settings.ALLOWED_HOSTS + if validate_host(host, allowed_hosts): + return host + else: + raise SuspiciousOperation( + "Invalid HTTP_HOST header (you may need to set ALLOWED_HOSTS): %s" % host) def get_full_path(self): # RFC 3986 requires query string arguments to be in the ASCII range. @@ -799,3 +800,43 @@ def str_to_unicode(s, encoding): else: return s +def validate_host(host, allowed_hosts): + """ + Validate the given host header value for this site. + + Check that the host looks valid and matches a host or host pattern in the + given list of ``allowed_hosts``. Any pattern beginning with a period + matches a domain and all its subdomains (e.g. ``.example.com`` matches + ``example.com`` and any subdomain), ``*`` matches anything, and anything + else must match exactly. + + Return ``True`` for a valid host, ``False`` otherwise. + + """ + # All validation is case-insensitive + host = host.lower() + + # Basic sanity check + if not host_validation_re.match(host): + return False + + # Validate only the domain part. + if host[-1] == ']': + # It's an IPv6 address without a port. + domain = host + else: + domain = host.rsplit(':', 1)[0] + + for pattern in allowed_hosts: + pattern = pattern.lower() + match = ( + pattern == '*' or + pattern.startswith('.') and ( + domain.endswith(pattern) or domain == pattern[1:] + ) or + pattern == domain + ) + if match: + return True + + return False diff --git a/django/test/utils.py b/django/test/utils.py index ed5ab590a7..6d6b6e177f 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -75,6 +75,9 @@ def setup_test_environment(): mail.original_email_backend = settings.EMAIL_BACKEND settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' + settings._original_allowed_hosts = settings.ALLOWED_HOSTS + settings.ALLOWED_HOSTS = ['*'] + mail.outbox = [] deactivate() @@ -93,6 +96,9 @@ def teardown_test_environment(): settings.EMAIL_BACKEND = mail.original_email_backend del mail.original_email_backend + settings.ALLOWED_HOSTS = settings._original_allowed_hosts + del settings._original_allowed_hosts + del mail.outbox |
