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. | ||||
| 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 # | ||||
| ############## | ||||
|   | ||||
| @@ -44,7 +44,7 @@ class ModPythonRequest(http.HttpRequest): | ||||
|         # 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: | ||||
|   | ||||
| @@ -158,9 +158,8 @@ class WSGIRequest(http.HttpRequest): | ||||
|         # 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'): | ||||
|   | ||||
| @@ -113,6 +113,7 @@ class CompatCookie(SimpleCookie): | ||||
|  | ||||
| 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 @@ class HttpRequest(object): | ||||
|             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' | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| ----------------------- | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,7 @@ from __future__ import with_statement | ||||
| 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 @@ class TrailingSlashURLTests(TestCase): | ||||
|         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): | ||||
|     """ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user