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>
This commit is contained in:
parent
f6018c1e63
commit
db5b75f10f
|
@ -636,6 +636,7 @@ SILENCED_SYSTEM_CHECKS = []
|
||||||
#######################
|
#######################
|
||||||
SECURE_BROWSER_XSS_FILTER = False
|
SECURE_BROWSER_XSS_FILTER = False
|
||||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||||
|
SECURE_CROSS_ORIGIN_OPENER_POLICY = 'same-origin'
|
||||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = False
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = False
|
||||||
SECURE_HSTS_PRELOAD = False
|
SECURE_HSTS_PRELOAD = False
|
||||||
SECURE_HSTS_SECONDS = 0
|
SECURE_HSTS_SECONDS = 0
|
||||||
|
|
|
@ -3,6 +3,9 @@ from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
from .. import Error, Tags, Warning, register
|
from .. import Error, Tags, Warning, register
|
||||||
|
|
||||||
|
CROSS_ORIGIN_OPENER_POLICY_VALUES = {
|
||||||
|
'same-origin', 'same-origin-allow-popups', 'unsafe-none',
|
||||||
|
}
|
||||||
REFERRER_POLICY_VALUES = {
|
REFERRER_POLICY_VALUES = {
|
||||||
'no-referrer', 'no-referrer-when-downgrade', 'origin',
|
'no-referrer', 'no-referrer-when-downgrade', 'origin',
|
||||||
'origin-when-cross-origin', 'same-origin', 'strict-origin',
|
'origin-when-cross-origin', 'same-origin', 'strict-origin',
|
||||||
|
@ -17,8 +20,8 @@ W001 = Warning(
|
||||||
"You do not have 'django.middleware.security.SecurityMiddleware' "
|
"You do not have 'django.middleware.security.SecurityMiddleware' "
|
||||||
"in your MIDDLEWARE so the SECURE_HSTS_SECONDS, "
|
"in your MIDDLEWARE so the SECURE_HSTS_SECONDS, "
|
||||||
"SECURE_CONTENT_TYPE_NOSNIFF, SECURE_BROWSER_XSS_FILTER, "
|
"SECURE_CONTENT_TYPE_NOSNIFF, SECURE_BROWSER_XSS_FILTER, "
|
||||||
"SECURE_REFERRER_POLICY, and SECURE_SSL_REDIRECT settings will have no "
|
"SECURE_REFERRER_POLICY, SECURE_CROSS_ORIGIN_OPENER_POLICY, "
|
||||||
"effect.",
|
"and SECURE_SSL_REDIRECT settings will have no effect.",
|
||||||
id='security.W001',
|
id='security.W001',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -119,6 +122,15 @@ E023 = Error(
|
||||||
id='security.E023',
|
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():
|
def _security_middleware():
|
||||||
return 'django.middleware.security.SecurityMiddleware' in settings.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:
|
if not values <= REFERRER_POLICY_VALUES:
|
||||||
return [E023]
|
return [E023]
|
||||||
return []
|
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 []
|
||||||
|
|
|
@ -17,6 +17,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||||
self.redirect_host = settings.SECURE_SSL_HOST
|
self.redirect_host = settings.SECURE_SSL_HOST
|
||||||
self.redirect_exempt = [re.compile(r) for r in settings.SECURE_REDIRECT_EXEMPT]
|
self.redirect_exempt = [re.compile(r) for r in settings.SECURE_REDIRECT_EXEMPT]
|
||||||
self.referrer_policy = settings.SECURE_REFERRER_POLICY
|
self.referrer_policy = settings.SECURE_REFERRER_POLICY
|
||||||
|
self.cross_origin_opener_policy = settings.SECURE_CROSS_ORIGIN_OPENER_POLICY
|
||||||
|
|
||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
path = request.path.lstrip("/")
|
path = request.path.lstrip("/")
|
||||||
|
@ -52,4 +53,9 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||||
if isinstance(self.referrer_policy, str) else self.referrer_policy
|
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
|
return response
|
||||||
|
|
|
@ -417,8 +417,9 @@ The following checks are run if you use the :option:`check --deploy` option:
|
||||||
:class:`django.middleware.security.SecurityMiddleware` in your
|
:class:`django.middleware.security.SecurityMiddleware` in your
|
||||||
:setting:`MIDDLEWARE` so the :setting:`SECURE_HSTS_SECONDS`,
|
:setting:`MIDDLEWARE` so the :setting:`SECURE_HSTS_SECONDS`,
|
||||||
:setting:`SECURE_CONTENT_TYPE_NOSNIFF`, :setting:`SECURE_BROWSER_XSS_FILTER`,
|
:setting:`SECURE_CONTENT_TYPE_NOSNIFF`, :setting:`SECURE_BROWSER_XSS_FILTER`,
|
||||||
:setting:`SECURE_REFERRER_POLICY`, and :setting:`SECURE_SSL_REDIRECT`
|
:setting:`SECURE_REFERRER_POLICY`,
|
||||||
settings will have no effect.
|
:setting:`SECURE_CROSS_ORIGIN_OPENER_POLICY`, and
|
||||||
|
:setting:`SECURE_SSL_REDIRECT` settings will have no effect.
|
||||||
* **security.W002**: You do not have
|
* **security.W002**: You do not have
|
||||||
:class:`django.middleware.clickjacking.XFrameOptionsMiddleware` in your
|
:class:`django.middleware.clickjacking.XFrameOptionsMiddleware` in your
|
||||||
:setting:`MIDDLEWARE`, so your pages will not be served with an
|
: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.
|
should consider enabling this header to protect user privacy.
|
||||||
* **security.E023**: You have set the :setting:`SECURE_REFERRER_POLICY` setting
|
* **security.E023**: You have set the :setting:`SECURE_REFERRER_POLICY` setting
|
||||||
to an invalid value.
|
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
|
The following checks verify that your security-related settings are correctly
|
||||||
configured:
|
configured:
|
||||||
|
|
|
@ -198,6 +198,7 @@ enabled or disabled with a setting.
|
||||||
|
|
||||||
* :setting:`SECURE_BROWSER_XSS_FILTER`
|
* :setting:`SECURE_BROWSER_XSS_FILTER`
|
||||||
* :setting:`SECURE_CONTENT_TYPE_NOSNIFF`
|
* :setting:`SECURE_CONTENT_TYPE_NOSNIFF`
|
||||||
|
* :setting:`SECURE_CROSS_ORIGIN_OPENER_POLICY`
|
||||||
* :setting:`SECURE_HSTS_INCLUDE_SUBDOMAINS`
|
* :setting:`SECURE_HSTS_INCLUDE_SUBDOMAINS`
|
||||||
* :setting:`SECURE_HSTS_PRELOAD`
|
* :setting:`SECURE_HSTS_PRELOAD`
|
||||||
* :setting:`SECURE_HSTS_SECONDS`
|
* :setting:`SECURE_HSTS_SECONDS`
|
||||||
|
@ -354,6 +355,43 @@ this setting are:
|
||||||
|
|
||||||
__ https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values
|
__ 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:
|
||||||
|
|
||||||
``X-Content-Type-Options: nosniff``
|
``X-Content-Type-Options: nosniff``
|
||||||
|
|
|
@ -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
|
sets the :ref:`x-content-type-options` header on all responses that do not
|
||||||
already have it.
|
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
|
.. setting:: SECURE_HSTS_INCLUDE_SUBDOMAINS
|
||||||
|
|
||||||
``SECURE_HSTS_INCLUDE_SUBDOMAINS``
|
``SECURE_HSTS_INCLUDE_SUBDOMAINS``
|
||||||
|
@ -3599,6 +3613,7 @@ HTTP
|
||||||
|
|
||||||
* :setting:`SECURE_BROWSER_XSS_FILTER`
|
* :setting:`SECURE_BROWSER_XSS_FILTER`
|
||||||
* :setting:`SECURE_CONTENT_TYPE_NOSNIFF`
|
* :setting:`SECURE_CONTENT_TYPE_NOSNIFF`
|
||||||
|
* :setting:`SECURE_CROSS_ORIGIN_OPENER_POLICY`
|
||||||
* :setting:`SECURE_HSTS_INCLUDE_SUBDOMAINS`
|
* :setting:`SECURE_HSTS_INCLUDE_SUBDOMAINS`
|
||||||
* :setting:`SECURE_HSTS_PRELOAD`
|
* :setting:`SECURE_HSTS_PRELOAD`
|
||||||
* :setting:`SECURE_HSTS_SECONDS`
|
* :setting:`SECURE_HSTS_SECONDS`
|
||||||
|
|
|
@ -229,7 +229,11 @@ Models
|
||||||
Requests and Responses
|
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
|
Security
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
|
@ -204,6 +204,7 @@ Ess
|
||||||
ETag
|
ETag
|
||||||
ETags
|
ETags
|
||||||
exe
|
exe
|
||||||
|
exfiltration
|
||||||
extensibility
|
extensibility
|
||||||
Facebook
|
Facebook
|
||||||
fallback
|
fallback
|
||||||
|
@ -608,6 +609,7 @@ sortable
|
||||||
spam
|
spam
|
||||||
spammers
|
spammers
|
||||||
spatialite
|
spatialite
|
||||||
|
Spectre
|
||||||
Springmeyer
|
Springmeyer
|
||||||
SQL
|
SQL
|
||||||
ssi
|
ssi
|
||||||
|
|
|
@ -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
|
``Referer`` header is set. See :ref:`the referrer policy section of the
|
||||||
security middleware reference <referrer-policy>` for details.
|
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
|
Session security
|
||||||
================
|
================
|
||||||
|
|
||||||
|
|
|
@ -504,3 +504,28 @@ class CSRFFailureViewTest(SimpleTestCase):
|
||||||
csrf.check_csrf_failure_view(None),
|
csrf.check_csrf_failure_view(None),
|
||||||
[Error(msg, id='security.E101')],
|
[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])
|
||||||
|
|
|
@ -282,3 +282,42 @@ class SecurityMiddlewareTest(SimpleTestCase):
|
||||||
"""
|
"""
|
||||||
response = self.process_response(headers={'Referrer-Policy': 'unsafe-url'})
|
response = self.process_response(headers={'Referrer-Policy': 'unsafe-url'})
|
||||||
self.assertEqual(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')
|
||||||
|
|
|
@ -38,6 +38,7 @@ class TestStartProjectSettings(SimpleTestCase):
|
||||||
self.assertEqual(headers, [
|
self.assertEqual(headers, [
|
||||||
b'Content-Length: 0',
|
b'Content-Length: 0',
|
||||||
b'Content-Type: text/html; charset=utf-8',
|
b'Content-Type: text/html; charset=utf-8',
|
||||||
|
b'Cross-Origin-Opener-Policy: same-origin',
|
||||||
b'Referrer-Policy: same-origin',
|
b'Referrer-Policy: same-origin',
|
||||||
b'X-Content-Type-Options: nosniff',
|
b'X-Content-Type-Options: nosniff',
|
||||||
b'X-Frame-Options: DENY',
|
b'X-Frame-Options: DENY',
|
||||||
|
|
Loading…
Reference in New Issue