In this post I describe the same-origin policy and describe how, despite its protections, your site could leak information via side-channel vectors in the web platform APIs. I then describe how you can use the Cross-Origin-Opener-Policy
(COOP) header to mitigate one such vulnerability that is available using the window.open()
and window.opener
APIs. I describe which values provide isolation in which scenarios, how they differ between opener and popup, and which values you should use depending on your requirements.
- What is an origin?
- Same-origin policy restrictions
- Leaking information across origins
- Protecting against
window.open
attacks usingCross-Origin-Opener-Policy
- Testing
Cross-Origin-Opener-Policy
with multiple scenarios - What
Cross-Origin-Opener-Policy
value should I use? - Rolling-out
Cross-Origin-Opener-Policy
safely with reporting - A half-way house:
restrict-properties
- Enabling additional features with
crossOriginIsolated
What is an origin?
Before we can go any further, it's first important to clarify what an "origin" is from a browser's point of view. The origin of two URLs are considered to be the same if they
- Have the same scheme i.e.
http
orhttps
- Have the same domain i.e.
example.com
,andrewlock.net
ormicrosoft.com
- Have the same subdomain i.e.
www.
- Have the same port (which may be implicit) i.e. port
80
forhttp
and443
forhttps
Note that the path of a URL, i.e. /some/path
in the URL http://www.example.org/some/path
is not part of the origin, neither is the querystring (?key=value
) or a fragment (#someid
).
But essentially, everything before the path in the URL must match for two URLs to be considered "same-origin". The only subtlety to that statement is that the port may be implicit (port 80
for http
and 443
for https
) or explicit, and the sites are still considered same-origin.
There is a similar, less-strict, concept called "same-site" that is used by same-site cookies, among other things.
The table below compares URLs to the origin http://www.example.org
so you can clearly see the difference between same-origin and cross-origin:
URL | Description | same-origin |
---|---|---|
http://www.example.org | Identical URL | ✅ |
http://www.example.org:80 | Identical URL (implicit port) | ✅ |
http://www.example.org:5000 | Different port | ❌ |
http://example.org | Different subdomain | ❌ |
http://sub.example.org | Different subdomain | ❌ |
https://www.example.org | Different scheme | ❌ |
http://www.example.evil | Different TLD | ❌ |
When two sites have the same origin, they are termed "same-origin"; when they have different origins, they are "cross-origin".
Same-origin policy restrictions
The same-origin policy is a key security mechanism used by browsers to restrict how resources loaded from one origin can interact with resources loaded from another origin.
One of the most common interactions people first run into is when you're sending cross-origin requests, as many of these are blocked unless you enable Cross Origin Resource Sharing (CORS). These restrictions come into play when you're making fetch()
requests in JavaScript for example.
Most of the time, developers interacting with cross-origin documents are trying to do completely legitimate things, and the same-origin policy can seem very restrictive. But the same-origin policy is vital for isolating your site from malicious sites. Without it, your website would be extremely vulnerable!
However, there are also cross-origin restrictions at the JavaScript API-level that don't involve fetch()
. On the face of it, that might sound a bit confusing—how can you interact with a different document using JavaScript without making requests? There are several APIs that allow you to do this, which generally provide a reference to the Window
object for a different document. For example:
iframe.contentWindow
returns theWindow
object of an iFrame.window.parent
returns theWindow
object of the parent window (when called from an iFrame for example).window.open()
allows you to open a new window, whether in a popup or a new tab, and returns a reference to theWindow
object of the opened document.window.opener
provides a reference to theWindow
object of the document that opened this one. This is available when the current document was opened via awindow.open()
call, or by clicking a link that has atarget
attribute.
When the documents are same-origin, then you have direct access to the other Window
but in cross-origin scenarios, you can only access a limited number of properties and methods, for example:
window.close()
window.focus()
window.postMessage()
window.length
window.closed
(read-only)window.opener
(read-only)window.frames
(read-only)
The exact available properties and methods vary by browser, but you can find a more complete list here. Nevertheless, even though cross-origin interactions are limited to this small subset, there are attacks that can use these APIs to leak information across contexts.
Leaking information across origins
One class of vulnerabilities, often called cross-site leaks (aka XS-Leaks), attack the side-channels built into the web platform to infer information about other sites. There have been some high-profile attacks, such as Meltdown and Spectre which leverage hardware bugs, but there are also leaks that come directly from the browser APIs.
One source of information comes via the window.length
property which returns the number of frames (iframe
s) that are in the document. This seems like a very benign property, but if this number changes based on properties of the user, then that's a source of information that could be abused by an attacker. As a concrete example, Facebook patched such a bug that could be used to leak all sorts of information about the user (as described in this post).
There are many other possible sources of information leaks (described in detail at https://xsleaks.dev/), but it's clear that even having mostly-readonly access to a cross-origin document is enough to expose yourself to vulnerabilities. To work around this, browsers introduced the Cross-Origin-Opener-Policy
header, to give you additional restrictions.
Protecting against window.open
attacks using Cross-Origin-Opener-Policy
Cross-Origin-Opener-Policy
(COOP) is a security header that you can return in your HTTP responses, which enables additional protections for your site when you call window.open
, or when a different document calls window.open
to open your site.
The Cross-Origin-Opener-Policy
(COOP) header currently has three possible values:
unsafe-none
—the default, unsafe value.same-origin
—the most safe valuesame-origin-allow-popups
—a middle-ground, which provides some protections.
I'll describe how each of these headers impact the behaviour of the JavaScript APIs shortly, but a word of warning, the language can get confusing, as we're describing the available interactions between two different sites:
- The opener—this is the site that called
window.open()
. This API returns aWindow
object which references the opened site - The opened—this is the site that was opened. It can reference the
Window
object of the opener using thewindow.opener
property.
Both the opener and opened sites could set different values for COOP, and the combination of the two controls whether the two documents are in the same browsing context or not. When documents are in different browsing contexts, then
window.opener
in the opened document returnsnull
, instead of theWindow
reference to the opener.- In the opener, the value returned by
window.open()
returns "default" values for some of the properties, such aswindow.closed
Not that these restrictions are in addition to the same-origin restrictions that also apply.
There's a lot of moving pieces here, so the follow sections describe various scenarios.
Testing Cross-Origin-Opener-Policy
with multiple scenarios
To test out the impact of the COOP header, I created two simple ASP.NET Core web apps which serve HTML pages.
- Web app 1, hosted at
http://localhost:5011
, serves an Index.html document which contains a single button, which runswindow.open()
to open a popup in web app 2. - Web app 2, hosted at
http://localhost:5005
, serves a Popup.html document, which attempts to readwindow.opener
.
The apps are nothing fancy, just basic HTML that shows the value of the Cross-Origin-Opener-Policy
used to serve the page and a button to click:
Each of the apps runs a small amount of JavaScript to test the isolation of the site. In the opener, web app 1 reads the value of window.closed
; in the isolated scenario, this will always return true
, whereas it will return the "correct" value when the documents are in a shared context:
// Add a button click handler
document.getElementById('open_btn')
.addEventListener('click', () => {
// Open the popup in a new window
const opened = window.open('http://localhost:5005/popup', '_blank', 'popup=true');
// Check whether we have the correct value for opened.closed
document.getElementById('opener_ele')
.innerHTML = 'Opened window - reference object <code>opened.closed</code> is ' + (opened.closed
? '<span style="color:red">TRUE</span>'
: '<span style="color:green">FALSE</span>');
});
On the popup (opened) side, we check the value of window.opener
. In the isolated context, this will be null
, while in the shared context scenario it will be the standard Window
object:
document.getElementById('opener_ele')
.innerHTML = '<code>window.opener</code> object is ' +
(window.opener
? '<span style="color:green">AVAILABLE</span>'
: '<span style="color:red">UNAVAILABLE</span>')
For each app I used my NetEscapades.AspNetCore.SecurityHeaders NuGet package to configure the Cross-Origin-Opener-Policy
for each web app with one of the three possible values. In the following sections we look at how the header impacts the behaviour of the apps.
The impact of Cross-Origin-Opener-Policy
on same-origin communication
We'll start with the easy case, when both the opener and opened documents have the same origin, both running on http://localhost:5011
. In this scenario, there are no same-origin restrictions, regardless of what Cross-Origin-Opener-Policy
you apply to either the opener or the popup.
In this scenario, window.closed
returns the correct value in the opener (false
) and the window.opener
value returns the "real" Window
object to the popup. This happens even with the most-restrictive header value of same-origin
:
Each page can interact completely unrestricted with the other, because they're both on the same origin. Where things get interesting is when the pages are cross origin.
The impact of Cross-Origin-Opener-Policy
on cross-origin communication
In this scenario, a website opens a page on a different origin.
We'll again start with the easiest scenario, where both sites are using Cross-Origin-Opener-Policy: unsafe-none
(or, more likely, not returning the header at all). In this scenario, there are no additional restrictions over the normal same-site restrictions. That means:
- ✔
window.opener
is available in the popup - ✔
opened.closed
reflects the correct value in the opener document
This scenario is the least secure one. Remember, if there's a malicious site, it could be either the site doing the opening, or it could be the site being opened in the popup (if your site is tricked into opening it). So although there are still same-origin restrictions for accessing the other site's Window
object, the side channel vulnerabilities are unmitigated.
At the other end of the scale, if either the opener or the opened/popup document return Cross-Origin-Opener-Policy: same-origin
, the result is the same:
- ❌
window.opener
isnull
in the popup - ❌
opened.closed
always returnstrue
in the opener document
The image below shows this in action: the opened.closed
property returns true
even though the popup is clearly open, while in the popup window.opener
is null
:
Not that you get the same behaviour when either of the sites uses same-origin
, regardless of what the other site uses. Whether or not the sites share a browsing context is a binary decision, so if either of the sites opts out of sharing a context, the result is the same.
The final option to consider is same-origin-allow-popups
, which is more subtle:
- When returned by the opened/popup site, it behaves exactly like
same-origin
, i.e. isolated browsing contexts. - When returned by the opener, then if the popup returns
unsafe-none
(or doesn't return a COOP header), they will share browsing context. If the popup returns anything else, then they're isolated.
So in the following, the opener has same-origin-allow-popups
while the popup has unsafe-none
, and you can see they are sharing a context, as the values are correct and available:
Whereas in the following, the popup does not have unsafe-none
(instead it has same-origin-allow-popups
), so they don't share a browsing context, and the objects are blocked:
As a reminder, if the popup has same-origin-allow-popups
, this is treated the same as same-origin
, so even if the opener has unsafe-none
, they do not share a browsing context
I'm not going to lie, I found it all quite confusing trying to understand the matrix of possibilities. However, if you consider just the cross-origin scnario, you can narrow the options down to a 3×3 table of options. In the table below, "Shared ✔" means that the window.opener
and opened
values are available, because the sites share a browsing context. In the "Isolated ❌" case, the sites are isolated.
Opened Cross-Origin-Opener-Policy | ||||
---|---|---|---|---|
unsafe-none / missing | same-origin-allow-popups | same-origin | ||
Opener COOP | unsafe-none / missing | Shared ✔ | Isolated ❌ | Isolated ❌ |
same-origin-allow-popups | Shared ✔ | Isolated ❌ | Isolated ❌ | |
same-origin | Isolated ❌ | Isolated ❌ | Isolated ❌ |
Cross-Origin-Opener-Policy
header in a cross-origin scenario What Cross-Origin-Opener-Policy
value should I use?
So if you've come this far, maybe you're looking for some advice. Which Cross-Origin-Opener-Policy
header value should you use?
Luckily, this is relatively easy: if you want to be the most secure, your should use same-origin
. This protects you from malicious popups in case you're tricked into calling window.open
, but it also protects you if a malicious site opens your site in a popup.
Unfortunately, things might not be that simple. Maybe you interact with a payment or authentication site that requires opening in a popup and retrieving a token. If that's the case, you likely won't be able to use same-origin
. For this scenario you can use same-origin-allow-popups
(and the target authentication/payment site must omit the COOP header or alternatively use unsafe-none
).
If you do choose to use same-origin-allow-popups
for your site then you'll still be protected if a malicious site opens your site in a popup, but you won't be protected from sites that you open with window.open
.
Rolling-out Cross-Origin-Opener-Policy
safely with reporting
Given that returning the Cross-Origin-Opener-Policy
header has the potential to break both the opened and opener sites, you obviously need to be careful when rolling it out. It's strongly recommended that you use the Reporting API built into browsers to identify any issues in production.
Before taking the plunge, you should also consider running the Cross-Origin-Opener-Policy
header in "report only" mode, by using the Cross-Origin-Opener-Policy-Report-Only
header instead. This header "simulates" the policy, and sends any violations via the reporting API. This gives you a chance to adjust your site and re-evaluate, before enforcing the policy for real.
A half-way house: restrict-properties
Throughout this post I've said there's only three values but Chrome actually briefly implemented a fourth value, restrict-properties
. This was only available for companies signing up for a trial, and was explicitly meant to handle the payment/authentication scenario I described previously, but relaxing restrictions from the popup's point of view. I don't go into more detail here, as it never made it out of the experimental phase as far as I can tell.
Enabling additional features with crossOriginIsolated
After the Meltdown and Spectre vulnerabilities were revealed, browser makers removed some of the features they relied on. These were later re-introduced, but were guarded so that they could only be introduced in a "cross-origin isolated" state (exposed as window.crossOriginIsolated
).
To be in a cross-origin isolated state, your site must:
- Return the
Cross-Origin-Opener-Policy
header with the valuesame-origin
. - Return the
Cross-Origin-Embedder-Policy
header with the valuerequire-corp
orcredentialless
. I'll talk more about this header in a separate post.
If you meet these requirements then you unlock some additional APIs:
- You can send
SharedArrayBuffer
viaWindow.postMessage()
calls. Performance.now()
gives better precision.Performance.measureUserAgentSpecificMemory()
is available.
So that's yet another reason to use Cross-Origin-Opener-Policy: same origin
for your sites if you can!
Summary
In this post I discussed the same-origin policy and how, despite its restrictions, your site could leak information via side-channel vectors in the web platform APIs. I then described how you can use Cross-Origin-Opener-Policy
(COOP) to mitigate vulnerabilities that are associated with the window.open()
and window.opener
APIs.
I described which header values provide isolation in which scenarios, how the behaviour differs between opener and popup, and which values you should use based on your requirements. Finally, I described the cross-origin isolated state which enables additional APIs and requires that you use the same-origin
value.