Fixed #14597 -- Added a SECURE_PROXY_SSL_HEADER setting for cases when you're behind a proxy that 'swallows' the fact that a request is HTTPS

git-svn-id: http://code.djangoproject.com/svn/django/trunk@17209 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Adrian Holovaty 2011-12-16 22:06:06 +00:00
parent 4d32e6abc2
commit 61f0aff811
6 changed files with 117 additions and 5 deletions

View File

@ -419,6 +419,15 @@ USE_X_FORWARDED_HOST = False
# actual WSGI application object. # actual WSGI application object.
WSGI_APPLICATION = None WSGI_APPLICATION = None
# If your Django app is behind a proxy that sets a header to specify secure
# connections, AND that proxy ensures that user-submitted headers with the
# same name are ignored (so that people can't spoof it), set this value to
# a tuple of (header_name, header_value). For any requests that come in with
# that header/value, request.is_secure() will return True.
# WARNING! Only set this if you fully understand what you're doing. Otherwise,
# you may be opening yourself up to a security risk.
SECURE_PROXY_SSL_HEADER = None
############## ##############
# MIDDLEWARE # # MIDDLEWARE #
############## ##############

View File

@ -44,7 +44,7 @@ class ModPythonRequest(http.HttpRequest):
# doesn't always happen, so rather than crash, we defensively encode it. # doesn't always happen, so rather than crash, we defensively encode it.
return '%s%s' % (self.path, self._req.args and ('?' + iri_to_uri(self._req.args)) or '') return '%s%s' % (self.path, self._req.args and ('?' + iri_to_uri(self._req.args)) or '')
def is_secure(self): def _is_secure(self):
try: try:
return self._req.is_https() return self._req.is_https()
except AttributeError: except AttributeError:

View File

@ -158,9 +158,8 @@ class WSGIRequest(http.HttpRequest):
# Rather than crash if this doesn't happen, we encode defensively. # Rather than crash if this doesn't happen, we encode defensively.
return '%s%s' % (self.path, self.environ.get('QUERY_STRING', '') and ('?' + iri_to_uri(self.environ.get('QUERY_STRING', ''))) or '') return '%s%s' % (self.path, self.environ.get('QUERY_STRING', '') and ('?' + iri_to_uri(self.environ.get('QUERY_STRING', ''))) or '')
def is_secure(self): def _is_secure(self):
return 'wsgi.url_scheme' in self.environ \ return 'wsgi.url_scheme' in self.environ and self.environ['wsgi.url_scheme'] == 'https'
and self.environ['wsgi.url_scheme'] == 'https'
def _get_request(self): def _get_request(self):
if not hasattr(self, '_request'): if not hasattr(self, '_request'):

View File

@ -113,6 +113,7 @@ class CompatCookie(SimpleCookie):
from django.conf import settings from django.conf import settings
from django.core import signing from django.core import signing
from django.core.exceptions import ImproperlyConfigured
from django.core.files import uploadhandler from django.core.files import uploadhandler
from django.http.multipartparser import MultiPartParser from django.http.multipartparser import MultiPartParser
from django.http.utils import * from django.http.utils import *
@ -251,9 +252,23 @@ class HttpRequest(object):
location = urljoin(current_uri, location) location = urljoin(current_uri, location)
return iri_to_uri(location) return iri_to_uri(location)
def is_secure(self): def _is_secure(self):
return os.environ.get("HTTPS") == "on" return os.environ.get("HTTPS") == "on"
def is_secure(self):
# First, check the SECURE_PROXY_SSL_HEADER setting.
if settings.SECURE_PROXY_SSL_HEADER:
try:
header, value = settings.SECURE_PROXY_SSL_HEADER
except ValueError:
raise ImproperlyConfigured('The SECURE_PROXY_SSL_HEADER setting must be a tuple containing two values.')
if self.META.get(header, None) == value:
return True
# Failing that, fall back to _is_secure(), which is a hook for
# subclasses to implement.
return self._is_secure()
def is_ajax(self): def is_ajax(self):
return self.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' return self.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'

View File

@ -1530,6 +1530,64 @@ better. ``django-admin.py startproject`` creates one automatically.
.. setting:: SEND_BROKEN_LINK_EMAILS .. setting:: SEND_BROKEN_LINK_EMAILS
SECURE_PROXY_SSL_HEADER
-----------------------
.. versionadded:: 1.4
Default: ``None``
A tuple representing a HTTP header/value combination that signifies a request
is secure. This controls the behavior of the request object's ``is_secure()``
method.
This takes some explanation. By default, ``is_secure()`` is able to determine
whether a request is secure by looking at whether the requested URL uses
"https://".
If your Django app is behind a proxy, though, the proxy may be "swallowing" the
fact that a request is HTTPS, using a non-HTTPS connection between the proxy
and Django. In this case, ``is_secure()`` would always return ``False`` -- even
for requests that were made via HTTPS by the end user.
In this situation, you'll want to configure your proxy to set a custom HTTP
header that tells Django whether the request came in via HTTPS, and you'll want
to set ``SECURE_PROXY_SSL_HEADER`` so that Django knows what header to look
for.
You'll need to set a tuple with two elements -- the name of the header to look
for and the required value. For example::
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
Here, we're telling Django that we trust the ``X-Forwarded-Protocol`` header
that comes from our proxy, and any time its value is ``'https'``, then the
request is guaranteed to be secure (i.e., it originally came in via HTTPS).
Obviously, you should *only* set this setting if you control your proxy or
have some other guarantee that it sets/strips this header appropriately.
Note that the header needs to be in the format as used by ``request.META`` --
all caps and likely starting with ``HTTP_``. (Remember, Django automatically
adds ``'HTTP_'`` to the start of x-header names before making the header
available in ``request.META``.)
.. warning::
**You will probably open security holes in your site if you set this without knowing what you're doing. Seriously.**
Make sure ALL of the following are true before setting this (assuming the
values from the example above):
* Your Django app is behind a proxy.
* Your proxy strips the 'X-Forwarded-Protocol' header from all incoming
requests. In other words, if end users include that header in their
requests, the proxy will discard it.
* Your proxy sets the 'X-Forwarded-Protocol' header and sends it to Django,
but only for requests that originally come in via HTTPS.
If any of those are not true, you should keep this setting set to ``None``
and find another way of determining HTTPS, perhaps via custom middleware.
SEND_BROKEN_LINK_EMAILS SEND_BROKEN_LINK_EMAILS
----------------------- -----------------------

View File

@ -3,6 +3,7 @@ from __future__ import with_statement
import os import os
from django.conf import settings, global_settings from django.conf import settings, global_settings
from django.http import HttpRequest
from django.test import TransactionTestCase, TestCase, signals from django.test import TransactionTestCase, TestCase, signals
from django.test.utils import override_settings from django.test.utils import override_settings
@ -209,6 +210,36 @@ class TrailingSlashURLTests(TestCase):
self.assertEqual('http://media.foo.com/stupid//', self.assertEqual('http://media.foo.com/stupid//',
self.settings_module.MEDIA_URL) self.settings_module.MEDIA_URL)
class SecureProxySslHeaderTest(TestCase):
settings_module = settings
def setUp(self):
self._original_setting = self.settings_module.SECURE_PROXY_SSL_HEADER
def tearDown(self):
self.settings_module.SECURE_PROXY_SSL_HEADER = self._original_setting
def test_none(self):
self.settings_module.SECURE_PROXY_SSL_HEADER = None
req = HttpRequest()
self.assertEqual(req.is_secure(), False)
def test_set_without_xheader(self):
self.settings_module.SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
req = HttpRequest()
self.assertEqual(req.is_secure(), False)
def test_set_with_xheader_wrong(self):
self.settings_module.SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
req = HttpRequest()
req.META['HTTP_X_FORWARDED_PROTOCOL'] = 'wrongvalue'
self.assertEqual(req.is_secure(), False)
def test_set_with_xheader_right(self):
self.settings_module.SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
req = HttpRequest()
req.META['HTTP_X_FORWARDED_PROTOCOL'] = 'https'
self.assertEqual(req.is_secure(), True)
class EnvironmentVariableTest(TestCase): class EnvironmentVariableTest(TestCase):
""" """