Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Fixed #14597 -- Added a SECURE_PROXY_SSL_HEADER setting for cases whe…

…n 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
  • Loading branch information...
commit 61f0aff811aa596fa62136852c59d47f988d1185 1 parent 4d32e6a
@adrianholovaty adrianholovaty authored
View
9 django/conf/global_settings.py
@@ -419,6 +419,15 @@
# actual WSGI application object.
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 #
##############
View
2  django/core/handlers/modpython.py
@@ -44,7 +44,7 @@ def get_full_path(self):
# 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 '')
- def is_secure(self):
+ def _is_secure(self):
try:
return self._req.is_https()
except AttributeError:
View
5 django/core/handlers/wsgi.py
@@ -158,9 +158,8 @@ def get_full_path(self):
# 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 '')
- def is_secure(self):
- return 'wsgi.url_scheme' in self.environ \
- and self.environ['wsgi.url_scheme'] == 'https'
+ def _is_secure(self):
+ return 'wsgi.url_scheme' in self.environ and self.environ['wsgi.url_scheme'] == 'https'
def _get_request(self):
if not hasattr(self, '_request'):
View
17 django/http/__init__.py
@@ -113,6 +113,7 @@ def __init__(self, *args, **kwargs):
from django.conf import settings
from django.core import signing
+from django.core.exceptions import ImproperlyConfigured
from django.core.files import uploadhandler
from django.http.multipartparser import MultiPartParser
from django.http.utils import *
@@ -251,9 +252,23 @@ def build_absolute_uri(self, location=None):
location = urljoin(current_uri, location)
return iri_to_uri(location)
- def is_secure(self):
+ def _is_secure(self):
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):
return self.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'
View
58 docs/ref/settings.txt
@@ -1530,6 +1530,64 @@ better. ``django-admin.py startproject`` creates one automatically.
.. 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
-----------------------
View
31 tests/regressiontests/settings_tests/tests.py
@@ -3,6 +3,7 @@
import os
from django.conf import settings, global_settings
+from django.http import HttpRequest
from django.test import TransactionTestCase, TestCase, signals
from django.test.utils import override_settings
@@ -209,6 +210,36 @@ def test_double_slash(self):
self.assertEqual('http://media.foo.com/stupid//',
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):
"""
Please sign in to comment.
Something went wrong with that request. Please try again.