How to set the new COEP, COOP, and CORP security headers in Django

Here are three new security headers on the block:
- Cross-Origin-Opener-Policy (COOP) (MDN)
- Cross-Origin-Resource-Policy (CORP) (MDN)
- Cross-Origin-Embedder-Policy (COEP) (MDN)
They don’t currently score you any points on the securityheaders.com checker, but they’re worth looking at to improve your site’s security.
Let’s briefly look at what they do, Django’s future support for them, and how you can add them today. For a longer description see Scott Helme’s article and the above MDN links.
Cross-Origin-Opener-Policy (COOP)
COOP isolates your origin in its own browsing context group. A browsing context group is a set of documents that have references to each other, and thus live in the same memory space.
COOP isolation prevents attacks like Spectre and Meltdown, which exploit flaws in CPU’s to read protected areas of the current memory space. Such attacks allow third party origins to steal your origin’s secrets, such as session cookies, because they share your browsing context group.
The initial response from browsers to Spectre and Meltdown was to disable the JavaScript API’s necessary to execute the attacks. This meant disabling the high resolution clock performance.now()
and the data-sharing API SharedArrayBuffer
. Setting a secure COOP header allows your site to re-enable those features.
Adding COOP comes at the cost of breaking use of the Window.opener
API, when used between origins. This property allows a window, or tab, to interact with the window that opened it. This interaction is the reason browsing context groups can contain several origins in the first place. Thankfully, use of window.opener
cross-origin is a rare feature, and there are normally better ways to achieve the same results.
The COOP header takes one of three values:
unsafe-none
- the current default behaviour of browsers. Different origins unsafely share the same browsing context group.same-origin
- the fully secure mode. The origin setting the header gets its own browsing context group.same-origin-allow-popups
- a middle ground. Your origin will share browsing context groups with other origins that don’t set COOP, or set it tounsafe-none
. You should only use this a stop-gap solution whilst migrating, since if you don’t control the other origins and they decide to add a COOP header, your interaction with them will break.
Django 4.0, due for release in 2022, will set the COOP header in SecurityMiddleware
, with a default value of 'same-origin'
. This is thanks to Ticket #31840, which was reported and started by meggles711, and completed by Tim Graham.
Cross-Origin-Resource-Policy (CORP)
The CORP header defines which origins can embed your resources (URL’s). This allows you to block other sites from loading your resources into their browsing context groups, giving them potential access to your private data.
The CORP header takes one of three values:
cross-origin
- the current default behaviour of browsers. Other origins can embed your resources.same-origin
- the most secure option. Resources are not accessible outside this origin.same-site
- for sites using subdomains. Setting this option allows other origins in your site, which normally means subdomains, to access your resources. A site is defined as all origins underneath a top-level domain - for example, bothexample.com
andimg.example.com
are on the same site.
If some of your URL’s should should be embeddable by other sites, you should set same-origin
by default and cross-origin
only on your public URL’s. For a more detailed explanation, see resourcepolicy.fyi.
Django will add support for CORP in Ticket #31923, which was again reported by meggles711 and may be worked on for Django 4.0.
Cross-Origin-Embedder-Policy (COEP)
COEP is the other side of CORP. It allows you require the cross-origin resources you load to explicitly allow you to do so.
It takes one of two values:
unsafe-none
- the current default behaviour of browsers. You allow your site to embed any other sites’ resources.require-corp
- the secure mode. Any cross-origin resources you load must explicitly allow you to load them. They may permit you with either the older CORS header, or CORP.
This header is harder to add since it requires all cross-origin resources you use to set CORP. Some common third party providers already set CORP, for example Google Fonts, but others are still catching up.
Django will add support for COEP in Ticket #31923, hopefully in Django 4.0.
Reporting Modes
Both COOP and COEP have reporting support. This allows you to add a report-to
clause, specifying a URL that the browser can send reports to on resources that failed to meet your policies.
COOP and COEP also support report-only modes, activated by adding Report-Only
on the end of the header name, e.g. Cross-Origin-Opener-Policy-Report-Only
. In this mode, the browser does not enforce the policy, but only sends reports on the failing resources. This is great for rolling out the header on larger sites.
For reporting support, you’ll need a service to receive and parse the reports, defined in the Report-To
header. Check out report-uri.com for such a service - I previously documented setting it up in this post.
Django Middleware
Built-in support for any of these headers won’t be coming until Django 4.0, but we can add them today with a custom middleware. For example, this middleware sets all headers to their most secure values, with reporting enabled for COEP and COOP:
class SecurityHeadersMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response["Cross-Origin-Embedder-Policy"] = 'require-corp; report-to="default"'
response["Cross-Origin-Opener-Policy"] = 'same-origin; report-to="default"'
response["Cross-Origin-Resource-Policy"] = "same-origin"
return response
We can add this middleware in MIDDLEWARE
next to Django’s SecurityMiddleware
, to set the headers at the same point in response processing. We’d also want to set Report-To
, with a group named “default”, as I covered previously.
This example uses the most secure values for all the headers. When deploying on a site using third party resources, we’d want to check whether they are setting the CORP header correctly, and use the report-only modes first to check pages continue to work.
Tests
We can test the above middleware like so:
from django.http import HttpResponse
from django.test import RequestFactory, SimpleTestCase
from db_buddy.core.middleware import SecurityHeadersMiddleware
class SecurityHeadersMiddlewareTests(SimpleTestCase):
def test_middleware(self):
def dummy_view(request):
return HttpResponse()
middleware = SecurityHeadersMiddleware(dummy_view)
request = RequestFactory().get("/")
response = middleware(request)
assert (
response["Cross-Origin-Embedder-Policy"]
== 'require-corp; report-to="default"'
)
assert response["Cross-Origin-Resource-Policy"] == "same-origin"
assert (
response["Cross-Origin-Opener-Policy"] == 'same-origin; report-to="default"'
)
We use a RequestFactory
to generate a request and check the response has the headers correctly set.
Improve your Django develompent experience with my new book.
One summary email a week, no spam, I pinky promise.
Related posts: