diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 9920c0391c..2274ea3f4f 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -633,5 +633,6 @@ SECURE_HSTS_INCLUDE_SUBDOMAINS = False SECURE_HSTS_PRELOAD = False SECURE_HSTS_SECONDS = 0 SECURE_REDIRECT_EXEMPT = [] +SECURE_REFERRER_POLICY = None SECURE_SSL_HOST = None SECURE_SSL_REDIRECT = False diff --git a/django/core/checks/security/base.py b/django/core/checks/security/base.py index dce2039a36..b69c2a11da 100644 --- a/django/core/checks/security/base.py +++ b/django/core/checks/security/base.py @@ -1,6 +1,12 @@ from django.conf import settings -from .. import Tags, Warning, register +from .. import Error, Tags, Warning, register + +REFERRER_POLICY_VALUES = { + 'no-referrer', 'no-referrer-when-downgrade', 'origin', + 'origin-when-cross-origin', 'same-origin', 'strict-origin', + 'strict-origin-when-cross-origin', 'unsafe-url', +} SECRET_KEY_MIN_LENGTH = 50 SECRET_KEY_MIN_UNIQUE_CHARACTERS = 5 @@ -8,9 +14,9 @@ SECRET_KEY_MIN_UNIQUE_CHARACTERS = 5 W001 = Warning( "You do not have 'django.middleware.security.SecurityMiddleware' " "in your MIDDLEWARE so the SECURE_HSTS_SECONDS, " - "SECURE_CONTENT_TYPE_NOSNIFF, " - "SECURE_BROWSER_XSS_FILTER, and SECURE_SSL_REDIRECT settings " - "will have no effect.", + "SECURE_CONTENT_TYPE_NOSNIFF, SECURE_BROWSER_XSS_FILTER, " + "SECURE_REFERRER_POLICY, and SECURE_SSL_REDIRECT settings will have no " + "effect.", id='security.W001', ) @@ -96,6 +102,19 @@ W021 = Warning( id='security.W021', ) +W022 = Warning( + 'You have not set the SECURE_REFERRER_POLICY setting. Without this, your ' + 'site will not send a Referrer-Policy header. You should consider ' + 'enabling this header to protect user privacy.', + id='security.W022', +) + +E023 = Error( + 'You have set the SECURE_REFERRER_POLICY setting to an invalid value.', + hint='Valid values are: {}.'.format(', '.join(sorted(REFERRER_POLICY_VALUES))), + id='security.E023', +) + def _security_middleware(): return 'django.middleware.security.SecurityMiddleware' in settings.MIDDLEWARE @@ -189,3 +208,18 @@ def check_xframe_deny(app_configs, **kwargs): @register(Tags.security, deploy=True) def check_allowed_hosts(app_configs, **kwargs): return [] if settings.ALLOWED_HOSTS else [W020] + + +@register(Tags.security, deploy=True) +def check_referrer_policy(app_configs, **kwargs): + if _security_middleware(): + if settings.SECURE_REFERRER_POLICY is None: + return [W022] + # Support a comma-separated string or iterable of values to allow fallback. + if isinstance(settings.SECURE_REFERRER_POLICY, str): + values = {v.strip() for v in settings.SECURE_REFERRER_POLICY.split(',')} + else: + values = set(settings.SECURE_REFERRER_POLICY) + if not values <= REFERRER_POLICY_VALUES: + return [E023] + return [] diff --git a/django/middleware/security.py b/django/middleware/security.py index dfca3b64de..c0877b350a 100644 --- a/django/middleware/security.py +++ b/django/middleware/security.py @@ -15,6 +15,7 @@ class SecurityMiddleware(MiddlewareMixin): self.redirect = settings.SECURE_SSL_REDIRECT self.redirect_host = settings.SECURE_SSL_HOST self.redirect_exempt = [re.compile(r) for r in settings.SECURE_REDIRECT_EXEMPT] + self.referrer_policy = settings.SECURE_REFERRER_POLICY self.get_response = get_response def process_request(self, request): @@ -43,4 +44,12 @@ class SecurityMiddleware(MiddlewareMixin): if self.xss_filter: response.setdefault('X-XSS-Protection', '1; mode=block') + if self.referrer_policy: + # Support a comma-separated string or iterable of values to allow + # fallback. + response.setdefault('Referrer-Policy', ','.join( + [v.strip() for v in self.referrer_policy.split(',')] + if isinstance(self.referrer_policy, str) else self.referrer_policy + )) + return response diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index f147d9dc0b..1289ffe1ab 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -342,7 +342,8 @@ The following checks are run if you use the :option:`check --deploy` option: :class:`django.middleware.security.SecurityMiddleware` in your :setting:`MIDDLEWARE` so the :setting:`SECURE_HSTS_SECONDS`, :setting:`SECURE_CONTENT_TYPE_NOSNIFF`, :setting:`SECURE_BROWSER_XSS_FILTER`, - and :setting:`SECURE_SSL_REDIRECT` settings will have no effect. + :setting:`SECURE_REFERRER_POLICY`, and :setting:`SECURE_SSL_REDIRECT` + settings will have no effect. * **security.W002**: You do not have :class:`django.middleware.clickjacking.XFrameOptionsMiddleware` in your :setting:`MIDDLEWARE`, so your pages will not be served with an @@ -428,6 +429,11 @@ The following checks are run if you use the :option:`check --deploy` option: * **security.W021**: You have not set the :setting:`SECURE_HSTS_PRELOAD` setting to ``True``. Without this, your site cannot be submitted to the browser preload list. +* **security.W022**: You have not set the :setting:`SECURE_REFERRER_POLICY` + setting. Without this, your site will not send a Referrer-Policy header. You + should consider enabling this header to protect user privacy. +* **security.E023**: You have set the :setting:`SECURE_REFERRER_POLICY` setting + to an invalid value. Signals ------- diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index db70a7c14d..04b598625e 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -186,6 +186,7 @@ enabled or disabled with a setting. * :setting:`SECURE_HSTS_PRELOAD` * :setting:`SECURE_HSTS_SECONDS` * :setting:`SECURE_REDIRECT_EXEMPT` +* :setting:`SECURE_REFERRER_POLICY` * :setting:`SECURE_SSL_HOST` * :setting:`SECURE_SSL_REDIRECT` @@ -241,6 +242,104 @@ If you wish to submit your site to the `browser preload list`_, set the __ https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security .. _browser preload list: https://hstspreload.org/ +.. _referrer-policy: + +Referrer Policy +~~~~~~~~~~~~~~~ + +.. versionadded:: 3.0 + +Browsers use `the Referer header`__ as a way to send information to a site +about how users got there. When a user clicks a link, the browser will send the +full URL of the linking page as the referrer. While this can be useful for some +purposes -- like figuring out who's linking to your site -- it also can cause +privacy concerns by informing one site that a user was visiting another site. + +__ https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer + +Some browsers have the ability to accept hints about whether they should send +the HTTP ``Referer`` header when a user clicks a link; this hint is provided +via `the Referrer-Policy header`__. This header can suggest any of three +behaviors to browsers: + +__ https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy + +* Full URL: send the entire URL in the ``Referer`` header. For example, if the + user is visiting ``https://example.com/page.html``, the ``Referer`` header + would contain ``"https://example.com/page.html"``. + +* Origin only: send only the "origin" in the referrer. The origin consists of + the scheme, host and (optionally) port number. For example, if the user is + visiting ``https://example.com/page.html``, the origin would be + ``https://example.com/``. + +* No referrer: do not send a ``Referer`` header at all. + +There are two types of conditions this header can tell a browser to watch out +for: + +* Same-origin versus cross-origin: a link from ``https://example.com/1.html`` + to ``https://example.com/2.html`` is same-origin. A link from + ``https://example.com/page.html`` to ``https://not.example.com/page.html`` is + cross-origin. + +* Protocol downgrade: a downgrade occurs if the page containing the link is + served via HTTPS, but the page being linked to is not served via HTTPS. + +.. warning:: + When your site is served via HTTPS, :ref:`Django's CSRF protection system + ` requires the ``Referer`` header to be present, so completely + disabling the ``Referer`` header will interfere with CSRF protection. To + gain most of the benefits of disabling ``Referer`` headers while also + keeping CSRF protection, consider enabling only same-origin referrers. + +``SecurityMiddleware`` can set the ``Referrer-Policy`` header for you, based on +the the :setting:`SECURE_REFERRER_POLICY` setting (note spelling: browsers send +a ``Referer`` header when a user clicks a link, but the header instructing a +browser whether to do so is spelled ``Referrer-Policy``). The valid values for +this setting are: + +``no-referrer`` + Instructs the browser to send no referrer for links clicked on this site. + +``no-referrer-when-downgrade`` + Instructs the browser to send a full URL as the referrer, but only when no + protocol downgrade occurs. + +``origin`` + Instructs the browser to send only the origin, not the full URL, as the + referrer. + +``origin-when-cross-origin`` + Instructs the browser to send the full URL as the referrer for same-origin + links, and only the origin for cross-origin links. + +``same-origin`` + Instructs the browser to send a full URL, but only for same-origin links. No + referrer will be sent for cross-origin links. + +``strict-origin`` + Instructs the browser to send only the origin, not the full URL, and to send + no referrer when a protocol downgrade occurs. + +``strict-origin-when-cross-origin`` + Instructs the browser to send the full URL when the link is same-origin and + no protocol downgrade occurs; send only the origin when the link is + cross-origin and no protocol downgrade occurs; and no referrer when a + protocol downgrade occurs. + +``unsafe-url`` + Instructs the browser to always send the full URL as the referrer. + +.. admonition:: Unknown Policy Values + + Where a policy value is `unknown`__ by a user agent, it is possible to + specify multiple policy values to provide a fallback. The last specified + value that is understood takes precedence. To support this, an iterable or + comma-separated string can be used with :setting:`SECURE_REFERRER_POLICY`. + + __ https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values + .. _x-content-type-options: ``X-Content-Type-Options: nosniff`` diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index aa9bc1ddb8..1ec8e9d94c 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -2319,6 +2319,19 @@ If a URL path matches a regular expression in this list, the request will not be redirected to HTTPS. If :setting:`SECURE_SSL_REDIRECT` is ``False``, this setting has no effect. +.. setting:: SECURE_REFERRER_POLICY + +``SECURE_REFERRER_POLICY`` +-------------------------- + +.. versionadded:: 3.0 + +Default: ``None`` + +If configured, the :class:`~django.middleware.security.SecurityMiddleware` sets +the :ref:`referrer-policy` header on all responses that do not already have it +to the value provided. + .. setting:: SECURE_SSL_HOST ``SECURE_SSL_HOST`` @@ -3500,6 +3513,7 @@ HTTP * :setting:`SECURE_HSTS_SECONDS` * :setting:`SECURE_PROXY_SSL_HEADER` * :setting:`SECURE_REDIRECT_EXEMPT` + * :setting:`SECURE_REFERRER_POLICY` * :setting:`SECURE_SSL_HOST` * :setting:`SECURE_SSL_REDIRECT` * :setting:`SIGNING_BACKEND` diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index a930a17768..9891119e66 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -380,6 +380,9 @@ Security :ref:`x-content-type-options` header on all responses that do not already have it. +* :class:`~django.middleware.security.SecurityMiddleware` can now send the + :ref:`Referrer-Policy ` header. + Serialization ~~~~~~~~~~~~~ diff --git a/docs/topics/security.txt b/docs/topics/security.txt index 862b2de258..8d749cc478 100644 --- a/docs/topics/security.txt +++ b/docs/topics/security.txt @@ -204,6 +204,15 @@ Additionally, Django requires you to explicitly enable support for the ``X-Forwarded-Host`` header (via the :setting:`USE_X_FORWARDED_HOST` setting) if your configuration requires it. +Referrer policy +=============== + +Browsers use the ``Referer`` header as a way to send information to a site +about how users got there. By setting a *Referrer Policy* you can help to +protect the privacy of your users, restricting under which circumstances the +``Referer`` header is set. See :ref:`the referrer policy section of the +security middleware reference ` for details. + Session security ================ diff --git a/tests/check_framework/test_security.py b/tests/check_framework/test_security.py index e6728606ef..4c1869d272 100644 --- a/tests/check_framework/test_security.py +++ b/tests/check_framework/test_security.py @@ -502,3 +502,46 @@ class CheckAllowedHostsTest(SimpleTestCase): @override_settings(ALLOWED_HOSTS=['.example.com']) def test_allowed_hosts_set(self): self.assertEqual(self.func(None), []) + + +class CheckReferrerPolicyTest(SimpleTestCase): + + @property + def func(self): + from django.core.checks.security.base import check_referrer_policy + return check_referrer_policy + + @override_settings( + MIDDLEWARE=['django.middleware.security.SecurityMiddleware'], + SECURE_REFERRER_POLICY=None, + ) + def test_no_referrer_policy(self): + self.assertEqual(self.func(None), [base.W022]) + + @override_settings(MIDDLEWARE=[], SECURE_REFERRER_POLICY=None) + def test_no_referrer_policy_no_middleware(self): + """ + Don't warn if SECURE_REFERRER_POLICY is None and SecurityMiddleware + isn't in MIDDLEWARE. + """ + self.assertEqual(self.func(None), []) + + @override_settings(MIDDLEWARE=['django.middleware.security.SecurityMiddleware']) + def test_with_referrer_policy(self): + tests = ( + 'strict-origin', + 'strict-origin,origin', + 'strict-origin, origin', + ['strict-origin', 'origin'], + ('strict-origin', 'origin'), + ) + for value in tests: + with self.subTest(value=value), override_settings(SECURE_REFERRER_POLICY=value): + self.assertEqual(self.func(None), []) + + @override_settings( + MIDDLEWARE=['django.middleware.security.SecurityMiddleware'], + SECURE_REFERRER_POLICY='invalid-value', + ) + def test_with_invalid_referrer_policy(self): + self.assertEqual(self.func(None), [base.E023]) diff --git a/tests/middleware/test_security.py b/tests/middleware/test_security.py index 86153f19ee..07b72fc73a 100644 --- a/tests/middleware/test_security.py +++ b/tests/middleware/test_security.py @@ -222,3 +222,36 @@ class SecurityMiddlewareTest(SimpleTestCase): """ ret = self.process_request("get", "/some/url") self.assertIsNone(ret) + + @override_settings(SECURE_REFERRER_POLICY=None) + def test_referrer_policy_off(self): + """ + With SECURE_REFERRER_POLICY set to None, the middleware does not add a + "Referrer-Policy" header to the response. + """ + self.assertNotIn('Referrer-Policy', self.process_response()) + + def test_referrer_policy_on(self): + """ + With SECURE_REFERRER_POLICY set to a valid value, the middleware adds a + "Referrer-Policy" header to the response. + """ + tests = ( + ('strict-origin', 'strict-origin'), + ('strict-origin,origin', 'strict-origin,origin'), + ('strict-origin, origin', 'strict-origin,origin'), + (['strict-origin', 'origin'], 'strict-origin,origin'), + (('strict-origin', 'origin'), 'strict-origin,origin'), + ) + for value, expected in tests: + with self.subTest(value=value), override_settings(SECURE_REFERRER_POLICY=value): + self.assertEqual(self.process_response()['Referrer-Policy'], expected) + + @override_settings(SECURE_REFERRER_POLICY='strict-origin') + def test_referrer_policy_already_present(self): + """ + The middleware will not override a "Referrer-Policy" header already + present in the response. + """ + response = self.process_response(headers={'Referrer-Policy': 'unsafe-url'}) + self.assertEqual(response['Referrer-Policy'], 'unsafe-url')