From 893cea211ae88c6f68a6c2c281890d6f63541286 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 10 Sep 2011 00:46:38 +0000 Subject: [PATCH] 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 --- django/conf/global_settings.py | 2 + django/http/__init__.py | 3 +- docs/ref/request-response.txt | 9 +-- docs/ref/settings.txt | 15 ++++- docs/topics/security.txt | 17 +++++ tests/regressiontests/requests/tests.py | 90 +++++++++++++++++++++++++ 6 files changed, 130 insertions(+), 6 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 10d61920ed..637b5f4d65 100644 --- a/django/conf/global_settings.py +++ b/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 # ############## diff --git a/django/http/__init__.py b/django/http/__init__.py index 4f1cda7785..5a68e03d9d 100644 --- a/django/http/__init__.py +++ b/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'] diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 39259c2ac3..88b3d771ef 100644 --- a/docs/ref/request-response.txt +++ b/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"`` diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 3faa378492..756799114d 100644 --- a/docs/ref/settings.txt +++ b/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`. \ No newline at end of file + This setting has been superseded by :setting:`IGNORABLE_404_URLS`. diff --git a/docs/topics/security.txt b/docs/topics/security.txt index d2e06f90a9..a7cfa02ee2 100644 --- a/docs/topics/security.txt +++ b/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 ========================== diff --git a/tests/regressiontests/requests/tests.py b/tests/regressiontests/requests/tests.py index 8bc81ffcf2..e96f3129be 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, 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()