From 1cf60ce6017d904024ee132f7edae0b4b821a954 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Mon, 7 Mar 2022 17:25:52 -0500 Subject: [PATCH] Fixed #33569 -- Added SECURE_PROXY_SSL_HEADER support for list of protocols in the header value. --- django/http/request.py | 3 ++- docs/ref/settings.txt | 18 ++++++++++++++---- docs/releases/4.1.txt | 3 +++ tests/settings_tests/tests.py | 20 ++++++++++++++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/django/http/request.py b/django/http/request.py index 0b64b1b2b9..c25fa4379a 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -261,7 +261,8 @@ class HttpRequest: ) header_value = self.META.get(header) if header_value is not None: - return "https" if header_value == secure_value else "http" + header_value, *_ = header_value.split(",", 1) + return "https" if header_value.strip() == secure_value else "http" return self._get_scheme() def is_secure(self): diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 73a4b8b649..ae1b15eab0 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -2442,8 +2442,17 @@ required value. For example:: SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') This tells Django to trust the ``X-Forwarded-Proto`` 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). +proxy and that the request is guaranteed to be secure (i.e., it originally came +in via HTTPS) when: + +* the header value is ``'https'``, or +* its initial, leftmost value is ``'https'`` in the case of a comma-separated + list of protocols (e.g. ``'https,http,http'``). + +.. versionchanged:: 4.1 + + Support for a comma-separated list of protocols in the header value was + added. You should *only* set this setting if you control your proxy or have some other guarantee that it sets/strips this header appropriately. @@ -2463,8 +2472,9 @@ available in ``request.META``.) * Your Django app is behind a proxy. * Your proxy strips the ``X-Forwarded-Proto`` header from all incoming - requests. In other words, if end users include that header in their - requests, the proxy will discard it. + requests, even when it contains a comma-separated list of protocols. In + other words, if end users include that header in their requests, the + proxy will discard it. * Your proxy sets the ``X-Forwarded-Proto`` header and sends it to Django, but only for requests that originally come in via HTTPS. diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt index a61d5e8e11..b8dbc36da0 100644 --- a/docs/releases/4.1.txt +++ b/docs/releases/4.1.txt @@ -293,6 +293,9 @@ Security * The new :setting:`SECRET_KEY_FALLBACKS` setting allows providing a list of values for secret key rotation. +* The :setting:`SECURE_PROXY_SSL_HEADER` setting now supports a comma-separated + list of protocols in the header value. + Serialization ~~~~~~~~~~~~~ diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py index 1b095533b1..f9ea7700bb 100644 --- a/tests/settings_tests/tests.py +++ b/tests/settings_tests/tests.py @@ -424,6 +424,26 @@ class SecureProxySslHeaderTest(SimpleTestCase): req.META["HTTP_X_FORWARDED_PROTO"] = "https" self.assertIs(req.is_secure(), True) + @override_settings(SECURE_PROXY_SSL_HEADER=("HTTP_X_FORWARDED_PROTO", "https")) + def test_set_with_xheader_leftmost_right(self): + req = HttpRequest() + req.META["HTTP_X_FORWARDED_PROTO"] = "https, http" + self.assertIs(req.is_secure(), True) + req.META["HTTP_X_FORWARDED_PROTO"] = "https , http" + self.assertIs(req.is_secure(), True) + + @override_settings(SECURE_PROXY_SSL_HEADER=("HTTP_X_FORWARDED_PROTO", "https")) + def test_set_with_xheader_leftmost_not_secure(self): + req = HttpRequest() + req.META["HTTP_X_FORWARDED_PROTO"] = "http, https" + self.assertIs(req.is_secure(), False) + + @override_settings(SECURE_PROXY_SSL_HEADER=("HTTP_X_FORWARDED_PROTO", "https")) + def test_set_with_xheader_multiple_not_secure(self): + req = HttpRequest() + req.META["HTTP_X_FORWARDED_PROTO"] = "http ,wrongvalue,http,http" + self.assertIs(req.is_secure(), False) + @override_settings(SECURE_PROXY_SSL_HEADER=("HTTP_X_FORWARDED_PROTO", "https")) def test_xheader_preferred_to_underlying_request(self): class ProxyRequest(HttpRequest):