mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	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:
		| @@ -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 # | ||||||
| ############## | ############## | ||||||
|   | |||||||
| @@ -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: | ||||||
|   | |||||||
| @@ -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'): | ||||||
|   | |||||||
| @@ -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' | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 | ||||||
| ----------------------- | ----------------------- | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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): | ||||||
|     """ |     """ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user