Skip to content

Commit

Permalink
Added protection against spoofing of X_FORWARDED_HOST headers. A secu…
Browse files Browse the repository at this point in the history
…rity announcement will be made shortly.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16758 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
freakboy3742 committed Sep 10, 2011
1 parent 0516ac3 commit 893cea2
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 6 deletions.
2 changes: 2 additions & 0 deletions django/conf/global_settings.py
Expand Up @@ -402,6 +402,8 @@
# Default X-Frame-Options header value
X_FRAME_OPTIONS = 'SAMEORIGIN'

USE_X_FORWARDED_HOST = False

##############
# MIDDLEWARE #
##############
Expand Down
3 changes: 2 additions & 1 deletion django/http/__init__.py
Expand Up @@ -194,7 +194,8 @@ def __repr__(self):
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']
Expand Down
9 changes: 5 additions & 4 deletions docs/ref/request-response.txt
Expand Up @@ -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"``

Expand Down
15 changes: 14 additions & 1 deletion docs/ref/settings.txt
Expand Up @@ -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
Expand Down Expand Up @@ -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 changes: 17 additions & 0 deletions docs/topics/security.txt
Expand Up @@ -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
==========================

Expand Down
90 changes: 90 additions & 0 deletions tests/regressiontests/requests/tests.py
Expand Up @@ -2,12 +2,14 @@
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()
Expand Down Expand Up @@ -97,6 +99,94 @@ def test_httprequest_location(self):
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()
Expand Down

0 comments on commit 893cea2

Please sign in to comment.