Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions django/conf/global_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -649,3 +649,4 @@ def gettext_noop(s):
SECURE_REFERRER_POLICY = 'same-origin'
SECURE_SSL_HOST = None
SECURE_SSL_REDIRECT = False
SECURE_CROSS_ORIGIN_OPENER_POLICY = 'same-origin'
31 changes: 29 additions & 2 deletions django/core/checks/security/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@
'strict-origin-when-cross-origin', 'unsafe-url',
}

CROSS_ORIGIN_OPENER_POLICY_VALUES = {
'same-origin', 'same-origin-allow-popups', 'unsafe-none',
}

SECRET_KEY_MIN_LENGTH = 50
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, "
"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',
)

Expand Down Expand Up @@ -116,6 +120,19 @@
id='security.E023',
)

W024 = Warning(
'You have not set the SECURE_CROSS_ORIGIN_OPENER_POLICY setting. Without this '
'your site will not send the Cross-Origin-Opener-Policy header. Consider '
'setting this header to protect your site from cross-origin attacks.',
id='security.W024',
)
E025 = Error(
'You have set the SECURE_CROSS_ORIGIN_OPENER_POLICY setting to an invalid '
'value.',
hint='Valid values are: {}.'.format(', '.join(CROSS_ORIGIN_OPENER_POLICY_VALUES)),
id='security.E025'
)

E100 = Error(
"DEFAULT_HASHING_ALGORITHM must be 'sha1' or 'sha256'.",
id='security.E100',
Expand Down Expand Up @@ -235,6 +252,16 @@ def check_referrer_policy(app_configs, **kwargs):
return []


@register(Tags.security, deploy=True)
def check_cross_origin_opener_policy(app_configs, **kwargs):
if _security_middleware():
if settings.SECURE_CROSS_ORIGIN_OPENER_POLICY is None:
return [W024]
if settings.SECURE_CROSS_ORIGIN_OPENER_POLICY not in CROSS_ORIGIN_OPENER_POLICY_VALUES:
return [E025]
return []


# RemovedInDjango40Warning
@register(Tags.security)
def check_default_hashing_algorithm(app_configs, **kwargs):
Expand Down
5 changes: 5 additions & 0 deletions django/middleware/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ def __init__(self, get_response=None):
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.coop = settings.SECURE_CROSS_ORIGIN_OPENER_POLICY
self.get_response = get_response

def process_request(self, request):
path = request.path.lstrip("/")
Expand Down Expand Up @@ -54,4 +56,7 @@ def process_response(self, request, response):
if isinstance(self.referrer_policy, str) else self.referrer_policy
))

if self.coop:
response.setdefault('Cross-Origin-Opener-Policy', self.coop)

return response
10 changes: 8 additions & 2 deletions docs/ref/checks.txt
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,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`,
:setting:`SECURE_REFERRER_POLICY`, and :setting:`SECURE_SSL_REDIRECT`
settings will have no effect.
:setting:`SECURE_REFERRER_POLICY`, :setting:`SECURE_SSL_REDIRECT`, and
:setting:`SECURE_CROSS_ORIGIN_OPENER_POLICY` 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
Expand Down Expand Up @@ -483,6 +483,12 @@ 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.W024**: You have not set the :setting:`SECURE_CROSS_ORIGIN_OPENER_POLICY`
setting. Without this, your site will not send the Cross-Origin-Opener-Policy
header. Consider setting this header to protect your site from cross-origin
attacks.
* **security.E025**: 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:
Expand Down
32 changes: 32 additions & 0 deletions docs/ref/middleware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,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`
Expand Down Expand Up @@ -338,6 +339,37 @@ this setting are:

__ https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values

.. _cross-origin-opener-policy:

Cross-Origin Opener Policy
~~~~~~~~~~~~~~~~~~~~~~~~~~

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 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 make data loaded into a shared browsing context
group vulnerable.

__ 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.

``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``
Expand Down
12 changes: 12 additions & 0 deletions docs/ref/settings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2273,6 +2273,17 @@ 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``
---------------------------------------

Default: ``'same-origin'``

If configured, 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``
Expand Down Expand Up @@ -3631,6 +3642,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`
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/3.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,9 @@ Security
``SECRET_KEY``, and then going on to access ``settings.SECRET_KEY`` will now
raise an :exc:`~django.core.exceptions.ImproperlyConfigured` exception.

* :class:`~django.middleware.security.SecurityMiddleware` can now send the
:ref:`Cross-Origin Opener Policy <cross-origin-opener-policy>` header.

Serialization
~~~~~~~~~~~~~

Expand Down
1 change: 1 addition & 0 deletions docs/spelling_wordlist
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ sortable
spam
spammers
spatialite
Spectre
Springmeyer
SQL
ssi
Expand Down
11 changes: 11 additions & 0 deletions docs/topics/security.txt
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,17 @@ 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
==========================

The cross-origin opener policy (COOP) header allows you to isolate a top-level
window from other documents by putting them in a different browsing context
group, so that they cannot directly interact with the top-level window. If a
document protected by COOP opens a popup window, the popup’s *window.opener*
property will be null. COOP is used to protect 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
================

Expand Down
35 changes: 35 additions & 0 deletions tests/check_framework/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,3 +464,38 @@ def test_with_referrer_policy(self):
)
def test_with_invalid_referrer_policy(self):
self.assertEqual(base.check_referrer_policy(None), [base.E023])


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), [base.W024])

@override_settings(MIDDLEWARE=[], SECURE_CROSS_ORIGIN_OPENER_POLICY=None)
def test_no_coop_no_middleware(self):
"""
Don't warn if SECURE_CROSS_ORIGIN_OPENER_POLICY is None and SecurityMiddleware
isn't in MIDDLEWARE.
"""
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.E025])
31 changes: 31 additions & 0 deletions tests/middleware/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,34 @@ def test_referrer_policy_already_present(self):
"""
response = self.process_response(headers={'Referrer-Policy': 'unsafe-url'})
self.assertEqual(response['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_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'),
('same-origin-allow-popups', 'same-origin-allow-popups'),
('unsafe-none', 'unsafe-none'),
)
for value, expected in tests:
with self.subTest(value=value), override_settings(SECURE_CROSS_ORIGIN_OPENER_POLICY=value):
self.assertEqual(self.process_response()['Cross-Origin-Opener-Policy'], expected)

@override_settings(SECURE_CROSS_ORIGIN_OPENER_POLICY='unsafe-none')
def test_coop_already_present(self):
"""
The middleware will not 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['Cross-Origin-Opener-Policy'], 'same-origin')
1 change: 1 addition & 0 deletions tests/project_template/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def test_middleware_headers(self):
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',
Expand Down