From 2f7fadc38efa58ac0a8f93f936b82332a199f396 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 10 Sep 2011 01:07:50 +0000 Subject: [PATCH] [1.3.X] Added protection against spoofing of X_FORWARDED_HOST headers. A security announcement will be made shortly. Backport of r16758 from trunk. git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.3.X@16761 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 2 + django/http/__init__.py | 3 +- docs/ref/request-response.txt | 9 +-- docs/ref/settings.txt | 13 ++++ tests/regressiontests/requests/tests.py | 90 +++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 5 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 97b7ac670f..5ef6886ac5 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -399,6 +399,8 @@ URL_VALIDATOR_USER_AGENT = "Django/%s (http://www.djangoproject.com)" % get_vers DEFAULT_TABLESPACE = '' DEFAULT_INDEX_TABLESPACE = '' +USE_X_FORWARDED_HOST = False + ############## # MIDDLEWARE # ############## diff --git a/django/http/__init__.py b/django/http/__init__.py index 6fccd24da0..68ac45d6e3 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -153,7 +153,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'] diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index eb34f1e147..d15a07cdae 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -191,10 +191,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 333`_. + 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`. .. _PEP 333: http://www.python.org/dev/peps/pep-0333/ diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 6d69a085d9..175e50818c 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1960,6 +1960,19 @@ in order to format numbers. See also :setting:`THOUSAND_SEPARATOR` and :setting:`NUMBER_GROUPING`. +.. 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 diff --git a/tests/regressiontests/requests/tests.py b/tests/regressiontests/requests/tests.py index 154c1fa07d..cd488e2fed 100644 --- a/tests/regressiontests/requests/tests.py +++ b/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 from django.utils import unittest from django.utils.http import cookie_date + class RequestsTests(unittest.TestCase): def test_httprequest(self): request = HttpRequest() @@ -58,6 +60,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()