From 406dba04e1482a308cad74e3d06c050c76ba2d16 Mon Sep 17 00:00:00 2001
From: Nick Pope <nick.pope@flightdataservices.com>
Date: Thu, 21 Mar 2019 21:33:41 +0000
Subject: [PATCH] Fixed #29406 -- Added support for Referrer-Policy header.

Thanks to James Bennett for the initial implementation.
---
 django/conf/global_settings.py         |  1 +
 django/core/checks/security/base.py    | 42 +++++++++--
 django/middleware/security.py          |  9 +++
 docs/ref/checks.txt                    |  8 ++-
 docs/ref/middleware.txt                | 99 ++++++++++++++++++++++++++
 docs/ref/settings.txt                  | 14 ++++
 docs/releases/3.0.txt                  |  3 +
 docs/topics/security.txt               |  9 +++
 tests/check_framework/test_security.py | 43 +++++++++++
 tests/middleware/test_security.py      | 33 +++++++++
 10 files changed, 256 insertions(+), 5 deletions(-)

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
+    <using-csrf>` 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 <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 <referrer-policy>` 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')