Browse Source

Added protection against spoofing of X_FORWARDED_HOST headers. A security announcement will be made shortly.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16758 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Russell Keith-Magee 13 years ago
parent
commit
893cea211a

+ 2 - 0
django/conf/global_settings.py

@@ -402,6 +402,8 @@ DEFAULT_INDEX_TABLESPACE = ''
 # Default X-Frame-Options header value
 X_FRAME_OPTIONS = 'SAMEORIGIN'
 
+USE_X_FORWARDED_HOST = False
+
 ##############
 # MIDDLEWARE #
 ##############

+ 2 - 1
django/http/__init__.py

@@ -194,7 +194,8 @@ class HttpRequest(object):
     def get_host(self):
         """Returns the HTTP host using the environment or request headers."""
         # We try three options, in order of decreasing preference.
-        if 'HTTP_X_FORWARDED_HOST' in self.META:
+        if settings.USE_X_FORWARDED_HOST and (
+            'HTTP_X_FORWARDED_HOST' in self.META):
             host = self.META['HTTP_X_FORWARDED_HOST']
         elif 'HTTP_HOST' in self.META:
             host = self.META['HTTP_HOST']

+ 5 - 4
docs/ref/request-response.txt

@@ -193,10 +193,11 @@ Methods
 
 .. method:: HttpRequest.get_host()
 
-    Returns the originating host of the request using information from the
-    ``HTTP_X_FORWARDED_HOST`` and ``HTTP_HOST`` headers (in that order). If
-    they don't provide a value, the method uses a combination of
-    ``SERVER_NAME`` and ``SERVER_PORT`` as detailed in :pep:`3333`.
+    Returns the originating host of the request using information from
+    the ``HTTP_X_FORWARDED_HOST`` (if enabled in the settings) and ``HTTP_HOST``
+    headers (in that order). If they don't provide a value, the method
+    uses a combination of ``SERVER_NAME`` and ``SERVER_PORT`` as
+    detailed in :pep:`3333`.
 
     Example: ``"127.0.0.1:8000"``
 

+ 14 - 1
docs/ref/settings.txt

@@ -2078,6 +2078,19 @@ When :setting:`USE_L10N` is set to ``True`` and if this is also set to
 See also :setting:`DECIMAL_SEPARATOR`, :setting:`NUMBER_GROUPING` and
 :setting:`THOUSAND_SEPARATOR`.
 
+.. setting:: USE_X_FORWARDED_HOST
+
+USE_X_FORWARDED_HOST
+--------------------
+
+.. versionadded:: 1.3.1
+
+Default: ``False``
+
+A boolean that specifies whether to use the X-Forwarded-Host header in
+preference to the Host header. This should only be enabled if a proxy
+which sets this header is in use.
+
 .. setting:: YEAR_MONTH_FORMAT
 
 YEAR_MONTH_FORMAT
@@ -2135,4 +2148,4 @@ IGNORABLE_404_STARTS
 --------------------
 
 .. deprecated:: 1.4
-   This setting has been superseded by :setting:`IGNORABLE_404_URLS`.
+   This setting has been superseded by :setting:`IGNORABLE_404_URLS`.

+ 17 - 0
docs/topics/security.txt

@@ -145,6 +145,23 @@ information is not leaked:
 
 .. _additional-security-topics:
 
+Host Headers and Virtual Hosting
+================================
+
+Django uses the Host header provided by the client to construct URLs
+in certain cases. While these values are sanitized to prevent Cross
+Site Scripting attacks, they can be used for Cross-Site Request
+Forgery and cache poisoning attacks in some circumstances. We
+recommend that users of Django ensure their web-server configuration
+always validates incoming HTTP Host headers against the expected host
+name, disallows requests with no Host header, and that the web server
+not be configured with a catch-all virtual host which forwards
+requests to a Django application.
+
+Additionally, as of 1.3.1, Django requires users to explicitly enable
+support for the X-Forwarded-Host header if their configuration
+requires it.
+
 Additional security topics
 ==========================
 

+ 90 - 0
tests/regressiontests/requests/tests.py

@@ -2,12 +2,14 @@ import time
 from datetime import datetime, timedelta
 from StringIO import StringIO
 
+from django.conf import settings
 from django.core.handlers.modpython import ModPythonRequest
 from django.core.handlers.wsgi import WSGIRequest, LimitedStream
 from django.http import HttpRequest, HttpResponse, parse_cookie, build_request_repr
 from django.utils import unittest
 from django.utils.http import cookie_date
 
+
 class RequestsTests(unittest.TestCase):
     def test_httprequest(self):
         request = HttpRequest()
@@ -97,6 +99,94 @@ class RequestsTests(unittest.TestCase):
         self.assertEqual(request.build_absolute_uri(location="/path/with:colons"),
             'http://www.example.com/path/with:colons')
 
+    def test_http_get_host(self):
+        old_USE_X_FORWARDED_HOST = settings.USE_X_FORWARDED_HOST
+        try:
+            settings.USE_X_FORWARDED_HOST = False
+
+            # Check if X_FORWARDED_HOST is provided.
+            request = HttpRequest()
+            request.META = {
+                u'HTTP_X_FORWARDED_HOST': u'forward.com',
+                u'HTTP_HOST': u'example.com',
+                u'SERVER_NAME': u'internal.com',
+                u'SERVER_PORT': 80,
+            }
+            # X_FORWARDED_HOST is ignored.
+            self.assertEqual(request.get_host(), 'example.com')
+
+            # Check if X_FORWARDED_HOST isn't provided.
+            request = HttpRequest()
+            request.META = {
+                u'HTTP_HOST': u'example.com',
+                u'SERVER_NAME': u'internal.com',
+                u'SERVER_PORT': 80,
+            }
+            self.assertEqual(request.get_host(), 'example.com')
+
+            # Check if HTTP_HOST isn't provided.
+            request = HttpRequest()
+            request.META = {
+                u'SERVER_NAME': u'internal.com',
+                u'SERVER_PORT': 80,
+            }
+            self.assertEqual(request.get_host(), 'internal.com')
+
+            # Check if HTTP_HOST isn't provided, and we're on a nonstandard port
+            request = HttpRequest()
+            request.META = {
+                u'SERVER_NAME': u'internal.com',
+                u'SERVER_PORT': 8042,
+            }
+            self.assertEqual(request.get_host(), 'internal.com:8042')
+
+        finally:
+            settings.USE_X_FORWARDED_HOST = old_USE_X_FORWARDED_HOST
+
+    def test_http_get_host_with_x_forwarded_host(self):
+        old_USE_X_FORWARDED_HOST = settings.USE_X_FORWARDED_HOST
+        try:
+            settings.USE_X_FORWARDED_HOST = True
+
+            # Check if X_FORWARDED_HOST is provided.
+            request = HttpRequest()
+            request.META = {
+                u'HTTP_X_FORWARDED_HOST': u'forward.com',
+                u'HTTP_HOST': u'example.com',
+                u'SERVER_NAME': u'internal.com',
+                u'SERVER_PORT': 80,
+            }
+            # X_FORWARDED_HOST is obeyed.
+            self.assertEqual(request.get_host(), 'forward.com')
+
+            # Check if X_FORWARDED_HOST isn't provided.
+            request = HttpRequest()
+            request.META = {
+                u'HTTP_HOST': u'example.com',
+                u'SERVER_NAME': u'internal.com',
+                u'SERVER_PORT': 80,
+            }
+            self.assertEqual(request.get_host(), 'example.com')
+
+            # Check if HTTP_HOST isn't provided.
+            request = HttpRequest()
+            request.META = {
+                u'SERVER_NAME': u'internal.com',
+                u'SERVER_PORT': 80,
+            }
+            self.assertEqual(request.get_host(), 'internal.com')
+
+            # Check if HTTP_HOST isn't provided, and we're on a nonstandard port
+            request = HttpRequest()
+            request.META = {
+                u'SERVER_NAME': u'internal.com',
+                u'SERVER_PORT': 8042,
+            }
+            self.assertEqual(request.get_host(), 'internal.com:8042')
+
+        finally:
+            settings.USE_X_FORWARDED_HOST = old_USE_X_FORWARDED_HOST
+
     def test_near_expiration(self):
         "Cookie will expire when an near expiration time is provided"
         response = HttpResponse()