Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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
Adrian Holovaty authored December 16, 2011
9  django/conf/global_settings.py
@@ -419,6 +419,15 @@
419 419
 # actual WSGI application object.
420 420
 WSGI_APPLICATION = None
421 421
 
  422
+# If your Django app is behind a proxy that sets a header to specify secure
  423
+# connections, AND that proxy ensures that user-submitted headers with the
  424
+# same name are ignored (so that people can't spoof it), set this value to
  425
+# a tuple of (header_name, header_value). For any requests that come in with
  426
+# that header/value, request.is_secure() will return True.
  427
+# WARNING! Only set this if you fully understand what you're doing. Otherwise,
  428
+# you may be opening yourself up to a security risk.
  429
+SECURE_PROXY_SSL_HEADER = None
  430
+
422 431
 ##############
423 432
 # MIDDLEWARE #
424 433
 ##############
2  django/core/handlers/modpython.py
@@ -44,7 +44,7 @@ def get_full_path(self):
44 44
         # doesn't always happen, so rather than crash, we defensively encode it.
45 45
         return '%s%s' % (self.path, self._req.args and ('?' + iri_to_uri(self._req.args)) or '')
46 46
 
47  
-    def is_secure(self):
  47
+    def _is_secure(self):
48 48
         try:
49 49
             return self._req.is_https()
50 50
         except AttributeError:
5  django/core/handlers/wsgi.py
@@ -158,9 +158,8 @@ def get_full_path(self):
158 158
         # Rather than crash if this doesn't happen, we encode defensively.
159 159
         return '%s%s' % (self.path, self.environ.get('QUERY_STRING', '') and ('?' + iri_to_uri(self.environ.get('QUERY_STRING', ''))) or '')
160 160
 
161  
-    def is_secure(self):
162  
-        return 'wsgi.url_scheme' in self.environ \
163  
-            and self.environ['wsgi.url_scheme'] == 'https'
  161
+    def _is_secure(self):
  162
+        return 'wsgi.url_scheme' in self.environ and self.environ['wsgi.url_scheme'] == 'https'
164 163
 
165 164
     def _get_request(self):
166 165
         if not hasattr(self, '_request'):
17  django/http/__init__.py
@@ -113,6 +113,7 @@ def __init__(self, *args, **kwargs):
113 113
 
114 114
 from django.conf import settings
115 115
 from django.core import signing
  116
+from django.core.exceptions import ImproperlyConfigured
116 117
 from django.core.files import uploadhandler
117 118
 from django.http.multipartparser import MultiPartParser
118 119
 from django.http.utils import *
@@ -251,9 +252,23 @@ def build_absolute_uri(self, location=None):
251 252
             location = urljoin(current_uri, location)
252 253
         return iri_to_uri(location)
253 254
 
254  
-    def is_secure(self):
  255
+    def _is_secure(self):
255 256
         return os.environ.get("HTTPS") == "on"
256 257
 
  258
+    def is_secure(self):
  259
+        # First, check the SECURE_PROXY_SSL_HEADER setting.
  260
+        if settings.SECURE_PROXY_SSL_HEADER:
  261
+            try:
  262
+                header, value = settings.SECURE_PROXY_SSL_HEADER
  263
+            except ValueError:
  264
+                raise ImproperlyConfigured('The SECURE_PROXY_SSL_HEADER setting must be a tuple containing two values.')
  265
+            if self.META.get(header, None) == value:
  266
+                return True
  267
+
  268
+        # Failing that, fall back to _is_secure(), which is a hook for
  269
+        # subclasses to implement.
  270
+        return self._is_secure()
  271
+
257 272
     def is_ajax(self):
258 273
         return self.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'
259 274
 
58  docs/ref/settings.txt
@@ -1530,6 +1530,64 @@ better. ``django-admin.py startproject`` creates one automatically.
1530 1530
 
1531 1531
 .. setting:: SEND_BROKEN_LINK_EMAILS
1532 1532
 
  1533
+SECURE_PROXY_SSL_HEADER
  1534
+-----------------------
  1535
+
  1536
+.. versionadded:: 1.4
  1537
+
  1538
+Default: ``None``
  1539
+
  1540
+A tuple representing a HTTP header/value combination that signifies a request
  1541
+is secure. This controls the behavior of the request object's ``is_secure()``
  1542
+method.
  1543
+
  1544
+This takes some explanation. By default, ``is_secure()`` is able to determine
  1545
+whether a request is secure by looking at whether the requested URL uses
  1546
+"https://".
  1547
+
  1548
+If your Django app is behind a proxy, though, the proxy may be "swallowing" the
  1549
+fact that a request is HTTPS, using a non-HTTPS connection between the proxy
  1550
+and Django. In this case, ``is_secure()`` would always return ``False`` -- even
  1551
+for requests that were made via HTTPS by the end user.
  1552
+
  1553
+In this situation, you'll want to configure your proxy to set a custom HTTP
  1554
+header that tells Django whether the request came in via HTTPS, and you'll want
  1555
+to set ``SECURE_PROXY_SSL_HEADER`` so that Django knows what header to look
  1556
+for.
  1557
+
  1558
+You'll need to set a tuple with two elements -- the name of the header to look
  1559
+for and the required value. For example::
  1560
+
  1561
+    SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
  1562
+
  1563
+Here, we're telling Django that we trust the ``X-Forwarded-Protocol`` header
  1564
+that comes from our proxy, and any time its value is ``'https'``, then the
  1565
+request is guaranteed to be secure (i.e., it originally came in via HTTPS).
  1566
+Obviously, you should *only* set this setting if you control your proxy or
  1567
+have some other guarantee that it sets/strips this header appropriately.
  1568
+
  1569
+Note that the header needs to be in the format as used by ``request.META`` --
  1570
+all caps and likely starting with ``HTTP_``. (Remember, Django automatically
  1571
+adds ``'HTTP_'`` to the start of x-header names before making the header
  1572
+available in ``request.META``.)
  1573
+
  1574
+.. warning::
  1575
+
  1576
+    **You will probably open security holes in your site if you set this without knowing what you're doing. Seriously.**
  1577
+
  1578
+    Make sure ALL of the following are true before setting this (assuming the
  1579
+    values from the example above):
  1580
+
  1581
+    * Your Django app is behind a proxy.
  1582
+    * Your proxy strips the 'X-Forwarded-Protocol' header from all incoming
  1583
+      requests. In other words, if end users include that header in their
  1584
+      requests, the proxy will discard it.
  1585
+    * Your proxy sets the 'X-Forwarded-Protocol' header and sends it to Django,
  1586
+      but only for requests that originally come in via HTTPS.
  1587
+
  1588
+    If any of those are not true, you should keep this setting set to ``None``
  1589
+    and find another way of determining HTTPS, perhaps via custom middleware.
  1590
+
1533 1591
 SEND_BROKEN_LINK_EMAILS
1534 1592
 -----------------------
1535 1593
 
31  tests/regressiontests/settings_tests/tests.py
@@ -3,6 +3,7 @@
3 3
 import os
4 4
 
5 5
 from django.conf import settings, global_settings
  6
+from django.http import HttpRequest
6 7
 from django.test import TransactionTestCase, TestCase, signals
7 8
 from django.test.utils import override_settings
8 9
 
@@ -209,6 +210,36 @@ def test_double_slash(self):
209 210
         self.assertEqual('http://media.foo.com/stupid//',
210 211
                          self.settings_module.MEDIA_URL)
211 212
 
  213
+class SecureProxySslHeaderTest(TestCase):
  214
+    settings_module = settings
  215
+
  216
+    def setUp(self):
  217
+        self._original_setting = self.settings_module.SECURE_PROXY_SSL_HEADER
  218
+
  219
+    def tearDown(self):
  220
+        self.settings_module.SECURE_PROXY_SSL_HEADER = self._original_setting
  221
+
  222
+    def test_none(self):
  223
+        self.settings_module.SECURE_PROXY_SSL_HEADER = None
  224
+        req = HttpRequest()
  225
+        self.assertEqual(req.is_secure(), False)
  226
+
  227
+    def test_set_without_xheader(self):
  228
+        self.settings_module.SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
  229
+        req = HttpRequest()
  230
+        self.assertEqual(req.is_secure(), False)
  231
+
  232
+    def test_set_with_xheader_wrong(self):
  233
+        self.settings_module.SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
  234
+        req = HttpRequest()
  235
+        req.META['HTTP_X_FORWARDED_PROTOCOL'] = 'wrongvalue'
  236
+        self.assertEqual(req.is_secure(), False)
  237
+
  238
+    def test_set_with_xheader_right(self):
  239
+        self.settings_module.SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
  240
+        req = HttpRequest()
  241
+        req.META['HTTP_X_FORWARDED_PROTOCOL'] = 'https'
  242
+        self.assertEqual(req.is_secure(), True)
212 243
 
213 244
 class EnvironmentVariableTest(TestCase):
214 245
     """

0 notes on commit 61f0aff

Please sign in to comment.
Something went wrong with that request. Please try again.