From e9d0ecedb4a66b28f5239865457b77eac37fa553 Mon Sep 17 00:00:00 2001
From: Tim Graham <timograham@gmail.com>
Date: Mon, 17 Oct 2016 12:14:49 -0400
Subject: Fixed CVE-2016-9014 -- Validated Host header when DEBUG=True.

This is a security fix.
---
 django/http/request.py  |  9 +++++----
 docs/ref/settings.txt   | 11 ++++++++---
 tests/requests/tests.py | 29 +++++++++++++++--------------
 3 files changed, 28 insertions(+), 21 deletions(-)

diff --git a/django/http/request.py b/django/http/request.py
index aee8a0d..d3d4c83 100644
--- a/django/http/request.py
+++ b/django/http/request.py
@@ -74,12 +74,13 @@ class HttpRequest(object):
             if server_port != ('443' if self.is_secure() else '80'):
                 host = '%s:%s' % (host, server_port)
 
-        # There is no hostname validation when DEBUG=True
-        if settings.DEBUG:
-            return host
+        # Allow variants of localhost if ALLOWED_HOSTS is empty and DEBUG=True.
+        allowed_hosts = settings.ALLOWED_HOSTS
+        if settings.DEBUG and not allowed_hosts:
+            allowed_hosts = ['localhost', '127.0.0.1', '[::1]']
 
         domain, port = split_domain_port(host)
-        if domain and validate_host(domain, settings.ALLOWED_HOSTS):
+        if domain and validate_host(domain, allowed_hosts):
             return host
         else:
             msg = "Invalid HTTP_HOST header: %r." % host
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index 8da2984..d084f51 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -108,9 +108,10 @@ If the ``Host`` header (or ``X-Forwarded-Host`` if
 list, the :meth:`django.http.HttpRequest.get_host()` method will raise
 :exc:`~django.core.exceptions.SuspiciousOperation`.
 
-When :setting:`DEBUG` is ``True`` or when running tests, host validation is
-disabled; any host will be accepted. Thus it's usually only necessary to set it
-in production.
+When :setting:`DEBUG` is ``True``, the host
+is validated against ``['localhost', '127.0.0.1', '[::1]']``.
+
+``ALLOWED_HOSTS`` is not checked when running tests.
 
 This validation only applies via :meth:`~django.http.HttpRequest.get_host()`;
 if your code accesses the ``Host`` header directly from ``request.META`` you
@@ -131,6 +132,10 @@ For example, if :setting:`ALLOWED_INCLUDE_ROOTS` is ``('/home/html', '/var/www')
 then ``{% ssi /home/html/foo.txt %}`` would work, but ``{% ssi /etc/passwd %}``
 wouldn't.
 
+    In older versions, ``ALLOWED_HOSTS`` wasn't checked if ``DEBUG=True``.
+    This was also changed in Django 1.10.3, 1.9.11, and 1.8.16 to prevent a
+    DNS rebinding attack.
+
 .. setting:: APPEND_SLASH
 
 APPEND_SLASH
diff --git a/tests/requests/tests.py b/tests/requests/tests.py
index 6364ab1..a33baaf 100644
--- a/tests/requests/tests.py
+++ b/tests/requests/tests.py
@@ -627,21 +627,22 @@ class HostValidationTests(SimpleTestCase):
                 request.get_host()
 
     @override_settings(DEBUG=True, ALLOWED_HOSTS=[])
-    def test_host_validation_disabled_in_debug_mode(self):
-        """If ALLOWED_HOSTS is empty and DEBUG is True, all hosts pass."""
-        request = HttpRequest()
-        request.META = {
-            'HTTP_HOST': 'example.com',
-        }
-        self.assertEqual(request.get_host(), 'example.com')
+    def test_host_validation_in_debug_mode(self):
+        """
+        If ALLOWED_HOSTS is empty and DEBUG is True, variants of localhost are
+        allowed.
+        """
+        valid_hosts = ['localhost', '127.0.0.1', '[::1]']
+        for host in valid_hosts:
+            request = HttpRequest()
+            request.META = {'HTTP_HOST': host}
+            self.assertEqual(request.get_host(), host)
 
-        # Invalid hostnames would normally raise a SuspiciousOperation,
-        # but we have DEBUG=True, so this check is disabled.
-        request = HttpRequest()
-        request.META = {
-            'HTTP_HOST': "invalid_hostname.com",
-        }
-        self.assertEqual(request.get_host(), "invalid_hostname.com")
+        # Other hostnames raise a SuspiciousOperation.
+        with self.assertRaises(SuspiciousOperation):
+            request = HttpRequest()
+            request.META = {'HTTP_HOST': 'example.com'}
+            request.get_host()
 
     @override_settings(ALLOWED_HOSTS=[])
     def test_get_host_suggestion_of_allowed_host(self):
