فهرست منبع

Fixed #31840 -- Added support for Cross-Origin Opener Policy header.

Thanks Adam Johnson and Tim Graham for the reviews.

Co-authored-by: Tim Graham <timograham@gmail.com>
bankc 4 سال پیش
والد
کامیت
db5b75f10f

+ 1 - 0
django/conf/global_settings.py

@@ -636,6 +636,7 @@ SILENCED_SYSTEM_CHECKS = []
 #######################
 SECURE_BROWSER_XSS_FILTER = False
 SECURE_CONTENT_TYPE_NOSNIFF = True
+SECURE_CROSS_ORIGIN_OPENER_POLICY = 'same-origin'
 SECURE_HSTS_INCLUDE_SUBDOMAINS = False
 SECURE_HSTS_PRELOAD = False
 SECURE_HSTS_SECONDS = 0

+ 25 - 2
django/core/checks/security/base.py

@@ -3,6 +3,9 @@ from django.core.exceptions import ImproperlyConfigured
 
 from .. import Error, Tags, Warning, register
 
+CROSS_ORIGIN_OPENER_POLICY_VALUES = {
+    'same-origin', 'same-origin-allow-popups', 'unsafe-none',
+}
 REFERRER_POLICY_VALUES = {
     'no-referrer', 'no-referrer-when-downgrade', 'origin',
     'origin-when-cross-origin', 'same-origin', 'strict-origin',
@@ -17,8 +20,8 @@ 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, "
-    "SECURE_REFERRER_POLICY, and SECURE_SSL_REDIRECT settings will have no "
-    "effect.",
+    "SECURE_REFERRER_POLICY, SECURE_CROSS_ORIGIN_OPENER_POLICY, "
+    "and SECURE_SSL_REDIRECT settings will have no effect.",
     id='security.W001',
 )
 
@@ -119,6 +122,15 @@ E023 = Error(
     id='security.E023',
 )
 
+E024 = Error(
+    'You have set the SECURE_CROSS_ORIGIN_OPENER_POLICY setting to an invalid '
+    'value.',
+    hint='Valid values are: {}.'.format(
+        ', '.join(sorted(CROSS_ORIGIN_OPENER_POLICY_VALUES)),
+    ),
+    id='security.E024',
+)
+
 
 def _security_middleware():
     return 'django.middleware.security.SecurityMiddleware' in settings.MIDDLEWARE
@@ -232,3 +244,14 @@ def check_referrer_policy(app_configs, **kwargs):
         if not values <= REFERRER_POLICY_VALUES:
             return [E023]
     return []
+
+
+@register(Tags.security, deploy=True)
+def check_cross_origin_opener_policy(app_configs, **kwargs):
+    if (
+        _security_middleware() and
+        settings.SECURE_CROSS_ORIGIN_OPENER_POLICY is not None and
+        settings.SECURE_CROSS_ORIGIN_OPENER_POLICY not in CROSS_ORIGIN_OPENER_POLICY_VALUES
+    ):
+        return [E024]
+    return []

+ 6 - 0
django/middleware/security.py

@@ -17,6 +17,7 @@ class SecurityMiddleware(MiddlewareMixin):
         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.cross_origin_opener_policy = settings.SECURE_CROSS_ORIGIN_OPENER_POLICY
 
     def process_request(self, request):
         path = request.path.lstrip("/")
@@ -52,4 +53,9 @@ class SecurityMiddleware(MiddlewareMixin):
                 if isinstance(self.referrer_policy, str) else self.referrer_policy
             ))
 
+        if self.cross_origin_opener_policy:
+            response.setdefault(
+                'Cross-Origin-Opener-Policy',
+                self.cross_origin_opener_policy,
+            )
         return response

+ 5 - 2
docs/ref/checks.txt

@@ -417,8 +417,9 @@ 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`,
-  :setting:`SECURE_REFERRER_POLICY`, and :setting:`SECURE_SSL_REDIRECT`
-  settings will have no effect.
+  :setting:`SECURE_REFERRER_POLICY`,
+  :setting:`SECURE_CROSS_ORIGIN_OPENER_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
@@ -510,6 +511,8 @@ The following checks are run if you use the :option:`check --deploy` option:
   should consider enabling this header to protect user privacy.
 * **security.E023**: You have set the :setting:`SECURE_REFERRER_POLICY` setting
   to an invalid value.
+* **security.E024**: You have set the
+  :setting:`SECURE_CROSS_ORIGIN_OPENER_POLICY` setting to an invalid value.
 
 The following checks verify that your security-related settings are correctly
 configured:

+ 38 - 0
docs/ref/middleware.txt

@@ -198,6 +198,7 @@ enabled or disabled with a setting.
 
 * :setting:`SECURE_BROWSER_XSS_FILTER`
 * :setting:`SECURE_CONTENT_TYPE_NOSNIFF`
+* :setting:`SECURE_CROSS_ORIGIN_OPENER_POLICY`
 * :setting:`SECURE_HSTS_INCLUDE_SUBDOMAINS`
 * :setting:`SECURE_HSTS_PRELOAD`
 * :setting:`SECURE_HSTS_SECONDS`
@@ -354,6 +355,43 @@ this setting are:
 
     __ https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values
 
+.. _cross-origin-opener-policy:
+
+Cross-Origin Opener Policy
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 4.0
+
+Some browsers have the ability to isolate top-level windows from other
+documents by putting them in a separate browsing context group based on the
+value of the `Cross-Origin Opener Policy`__ (COOP) header. If a document that
+is isolated in this way opens a cross-origin popup window, the popup’s
+``window.opener`` property will be ``null``. Isolating windows using COOP is a
+defense-in-depth protection against cross-origin attacks, especially those like
+Spectre which allowed exfiltration of data loaded into a shared browsing
+context.
+
+__ https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy
+
+``SecurityMiddleware`` can set the ``Cross-Origin-Opener-Policy`` header for
+you, based on the :setting:`SECURE_CROSS_ORIGIN_OPENER_POLICY` setting. The
+valid values for this setting are:
+
+``same-origin``
+    Isolates the browsing context exclusively to same-origin documents.
+    Cross-origin documents are not loaded in the same browsing context. This
+    is the default and most secure option.
+
+``same-origin-allow-popups``
+    Isolates the browsing context to same-origin documents or those which
+    either don't set COOP or which opt out of isolation by setting a COOP of
+    ``unsafe-none``.
+
+``unsafe-none``
+    Allows the document to be added to its opener's browsing context group
+    unless the opener itself has a COOP of ``same-origin`` or
+    ``same-origin-allow-popups``.
+
 .. _x-content-type-options:
 
 ``X-Content-Type-Options: nosniff``

+ 15 - 0
docs/ref/settings.txt

@@ -2262,6 +2262,20 @@ If ``True``, the :class:`~django.middleware.security.SecurityMiddleware`
 sets the :ref:`x-content-type-options` header on all responses that do not
 already have it.
 
+.. setting:: SECURE_CROSS_ORIGIN_OPENER_POLICY
+
+``SECURE_CROSS_ORIGIN_OPENER_POLICY``
+-------------------------------------
+
+.. versionadded:: 4.0
+
+Default: ``'same-origin'``
+
+Unless set to ``None``, the
+:class:`~django.middleware.security.SecurityMiddleware` sets the
+:ref:`cross-origin-opener-policy` header on all responses that do not already
+have it to the value provided.
+
 .. setting:: SECURE_HSTS_INCLUDE_SUBDOMAINS
 
 ``SECURE_HSTS_INCLUDE_SUBDOMAINS``
@@ -3599,6 +3613,7 @@ HTTP
 
   * :setting:`SECURE_BROWSER_XSS_FILTER`
   * :setting:`SECURE_CONTENT_TYPE_NOSNIFF`
+  * :setting:`SECURE_CROSS_ORIGIN_OPENER_POLICY`
   * :setting:`SECURE_HSTS_INCLUDE_SUBDOMAINS`
   * :setting:`SECURE_HSTS_PRELOAD`
   * :setting:`SECURE_HSTS_SECONDS`

+ 5 - 1
docs/releases/4.0.txt

@@ -229,7 +229,11 @@ Models
 Requests and Responses
 ~~~~~~~~~~~~~~~~~~~~~~
 
-* ...
+* The :class:`~django.middleware.security.SecurityMiddleware` now adds the
+  :ref:`Cross-Origin Opener Policy <cross-origin-opener-policy>` header with a
+  value of ``'same-origin'`` to prevent cross-origin popups from sharing the
+  same browsing context. You can prevent this header from being added by
+  setting the :setting:`SECURE_CROSS_ORIGIN_OPENER_POLICY` setting to ``None``.
 
 Security
 ~~~~~~~~

+ 2 - 0
docs/spelling_wordlist

@@ -204,6 +204,7 @@ Ess
 ETag
 ETags
 exe
+exfiltration
 extensibility
 Facebook
 fallback
@@ -608,6 +609,7 @@ sortable
 spam
 spammers
 spatialite
+Spectre
 Springmeyer
 SQL
 ssi

+ 13 - 0
docs/topics/security.txt

@@ -213,6 +213,19 @@ 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.
 
+Cross-origin opener policy
+==========================
+
+.. versionadded:: 4.0
+
+The cross-origin opener policy (COOP) header allows browsers to isolate a
+top-level window from other documents by putting them in a different context
+group so that they cannot directly interact with the top-level window. If a
+document protected by COOP opens a cross-origin popup window, the popup’s
+``window.opener`` property will be ``null``. COOP protects against cross-origin
+attacks. See :ref:`the cross-origin opener policy section of the security
+middleware reference <cross-origin-opener-policy>` for details.
+
 Session security
 ================
 

+ 25 - 0
tests/check_framework/test_security.py

@@ -504,3 +504,28 @@ class CSRFFailureViewTest(SimpleTestCase):
             csrf.check_csrf_failure_view(None),
             [Error(msg, id='security.E101')],
         )
+
+
+class CheckCrossOriginOpenerPolicyTest(SimpleTestCase):
+    @override_settings(
+        MIDDLEWARE=['django.middleware.security.SecurityMiddleware'],
+        SECURE_CROSS_ORIGIN_OPENER_POLICY=None,
+    )
+    def test_no_coop(self):
+        self.assertEqual(base.check_cross_origin_opener_policy(None), [])
+
+    @override_settings(MIDDLEWARE=['django.middleware.security.SecurityMiddleware'])
+    def test_with_coop(self):
+        tests = ['same-origin', 'same-origin-allow-popups', 'unsafe-none']
+        for value in tests:
+            with self.subTest(value=value), override_settings(
+                SECURE_CROSS_ORIGIN_OPENER_POLICY=value,
+            ):
+                self.assertEqual(base.check_cross_origin_opener_policy(None), [])
+
+    @override_settings(
+        MIDDLEWARE=['django.middleware.security.SecurityMiddleware'],
+        SECURE_CROSS_ORIGIN_OPENER_POLICY='invalid-value',
+    )
+    def test_with_invalid_coop(self):
+        self.assertEqual(base.check_cross_origin_opener_policy(None), [base.E024])

+ 39 - 0
tests/middleware/test_security.py

@@ -282,3 +282,42 @@ class SecurityMiddlewareTest(SimpleTestCase):
         """
         response = self.process_response(headers={'Referrer-Policy': 'unsafe-url'})
         self.assertEqual(response.headers['Referrer-Policy'], 'unsafe-url')
+
+    @override_settings(SECURE_CROSS_ORIGIN_OPENER_POLICY=None)
+    def test_coop_off(self):
+        """
+        With SECURE_CROSS_ORIGIN_OPENER_POLICY set to None, the middleware does
+        not add a "Cross-Origin-Opener-Policy" header to the response.
+        """
+        self.assertNotIn('Cross-Origin-Opener-Policy', self.process_response())
+
+    def test_coop_default(self):
+        """SECURE_CROSS_ORIGIN_OPENER_POLICY defaults to same-origin."""
+        self.assertEqual(
+            self.process_response().headers['Cross-Origin-Opener-Policy'],
+            'same-origin',
+        )
+
+    def test_coop_on(self):
+        """
+        With SECURE_CROSS_ORIGIN_OPENER_POLICY set to a valid value, the
+        middleware adds a "Cross-Origin_Opener-Policy" header to the response.
+        """
+        tests = ['same-origin', 'same-origin-allow-popups', 'unsafe-none']
+        for value in tests:
+            with self.subTest(value=value), override_settings(
+                SECURE_CROSS_ORIGIN_OPENER_POLICY=value,
+            ):
+                self.assertEqual(
+                    self.process_response().headers['Cross-Origin-Opener-Policy'],
+                    value,
+                )
+
+    @override_settings(SECURE_CROSS_ORIGIN_OPENER_POLICY='unsafe-none')
+    def test_coop_already_present(self):
+        """
+        The middleware doesn't override a "Cross-Origin-Opener-Policy" header
+        already present in the response.
+        """
+        response = self.process_response(headers={'Cross-Origin-Opener-Policy': 'same-origin'})
+        self.assertEqual(response.headers['Cross-Origin-Opener-Policy'], 'same-origin')

+ 1 - 0
tests/project_template/test_settings.py

@@ -38,6 +38,7 @@ class TestStartProjectSettings(SimpleTestCase):
             self.assertEqual(headers, [
                 b'Content-Length: 0',
                 b'Content-Type: text/html; charset=utf-8',
+                b'Cross-Origin-Opener-Policy: same-origin',
                 b'Referrer-Policy: same-origin',
                 b'X-Content-Type-Options: nosniff',
                 b'X-Frame-Options: DENY',