Repository navigation
Restricting cross-origin WindowProxy access (Cross-Origin-Opener-Policy) #3740
Description
Proposal
Add HTTP header called Cross-Origin-Window-Policy, which takes a value of Deny, Allow, and Allow-PostMessage.
When the HTTP response of a document has a Cross-Origin-Window-Policy header, and the value case-insensitively matches Deny ignoring cases, the document is said to be fully isolated. If the value case-insensitively matches Allow-PostMessage ignoring cases, the document is said to be isolated with messaging. If the value doesn't match either or isn't set, then the document is said to be not isolated. If a document is fully isolated or *isolated with messaging", it is said to be isolated.
In a fully isolated document, access to any property on the window proxy of a cross-origin document (regardless of whether the target document is fully isolated or not) results in a SecurityError. In a document isolated with messaging, access to any property except postMessage on the window proxy of a cross-origin document results in a SecurityError. The restriction between two documents are symmetrical and the most stricter of the two prevails.
Furthermore, a new step is inserted into the concept of allowed to navigate before step 1: If B and/or A is isolated and A and B are not of the same origin, return false.
Examples
Let document A and document B be distinct documents its own browsing contexts. If A and B are of the same origin, the header has no effect. If A and B are cross-origin, then:
-
If document B is fully isolated and document A is not isolated. Any attempt to access a property on document B's window from document A results in a
SecurityError. Any attempt to access a property on document A's window from document B also results in aSecurityError`. -
If document B is isolated with messaging and document A is not isolated. Any attempt to access a property except
postMessageon document B's window from document A results in aSecurityError. Any attempt to access a property exceptpostMessageon document A's window from document B results in aSecurityError. -
If document B is isolated with messaging and document A is fully isolated. Any attempt to access a property on document B's window from document A results in a
SecurityError. Any attempt to access a property on document A's window from document B results in aSecurityError. -
If document B is isolated with messaging and document A is isolated with messaging. Any attempt to access a property except
postMessageon document B's window from document A results in aSecurityError. Any attempt to access a property exceptpostMessageon document A's window from document B results in aSecurityError.
Spectre Protection Plan
For the purpose of protecting a website a.com from Spectre in browsers which support process swap for top-level navigations without frame-level process isolation, a.com can set this header on all of its documents (not setting on some would result in leaks; more on this later).
If this header is set on a.com, we can swap process on cross-origin navigation from or to a.com's documents because this header guarantees that a.com doesn't have access to any other document outside of its origin, and vice versa.
Let's say we're on some page B1 in b.com, and it window.open'ed (isolated) a.com. Then b.com doesn't have access to a.com, and b.com doesn't have access to a.com so we can put them into two different processes. Obviously, a.com's iframes don't have access to b.com's frame tree either so if a website is currently relying on being able to do this, they won't be able to use this header.
Let's say now a.com is navigated to some other page B2 in b.com. In this case, the browser finds the process which loaded B1 and load B2 in the same process so that they can talk to one another via window proxies.
Activity
domenic commentedon Jun 5, 2018
/cc @whatwg/security. Very interesting...
bzbarsky commentedon Jun 5, 2018
Note that there are several different proposals in this space last I checked. @mystor was working on one too, somewhat different from the one above.
rniwa commentedon Jun 5, 2018
Note that this feature has been implemented in Safari 12: https://developer.apple.com/safari/whats-new/
Add 'Cross-Origin-Window-Policy' support - whatwg/html#3740
mystor commentedon Jun 21, 2018
I believe that in order to make this header allow all toplevel isolated documents to be put in a distinct process, we need to be more restrictive as to what we let same-origin but isolated documents do.
Mini Window Agent Explainer
Before I started talking to @annevk about this stuff, I wasn't familiar with Window Agents, so I figured I'd include a small explainer:
Window Agents are the set of window globals which do, or may dynamically, have access to each-other's objects (other than the cross-origin
WindowProxyandLocationobjects). A Window Agent is in an Agent Cluster which also includes dedicated workers. Currently, two globalshave the same Window Agent if they are loaded in Related Browsing Contexts and are Same Site.
Two globals in the same Agent Cluster must be loaded in the same process, even with complete Site Isolation. They may share objects,
SharedArrayBuffer(even overBroadcastChannel), and share a single event loop.Isolated/Non-Isolated Interaction
In this case, under the current proposal, we cannot actually put the isolated B-1 into a separate process. The reason for this is that A could embed a non-isolated iframe B-2, which would be in B-1's Window Agent according to the current logic.
This forces A to be same process with B-1, in case there exists a document B-2 which could be loaded, for example:
To fix this we need to prevent isolated and non-isolated documents from being in the same Window Agent, otherwise they can do the following:
This brings us to the first restriction I think we have to make, namely:
Isolated/Isolated Interaction
Unfortunately, that restriction is not sufficient. If
window.openerreferences a Nested Browsing Context, we still run into problems with moving the document out of proces.For example, consider the case where a document B loads a frame C which, in turn, uses
window.opento open a new Toplevel Browsing Context containing an isolated document A:In this situation, we cannot put A-1 in another process, as B could navigate the iframe C to be A-2, an isolated iframe which is
Same Origin-Domainto A-1:A-1 and A-2 must be in the same Window Agent, and thus the same process, as they are allowed to communicate, but we have no ability to move the iframe C out of process when loading A-2 in it, as we don't have OOP iframes.
This brings us to the second restriction I think we have to make, namely:
Shared Array Buffer
With those two restrictions, things are looking pretty good. Unfortunately,
SharedArrayBufferstrikes again.SharedArrayBuffercan be sent overBroadcastChannel, and only requires two globals to share an Agent Cluster. Each of our Window Agents has a corresponding Agent Cluster, which also contains off-main-thread dedicated workers.Unfortunately this breaks our isolation story again, for example:
In this case, B-1 and B-2 are Same-Site, so we have 2 Window Actors:
{ A }and{ B-1, B-2 }. This, again, forces us to load A in the same process as B-1 despite being isolated, in case it chooses to frame a document which shares a Window Actor with its opener.Fortunately for us, A currently hides its opener from other documents due to its (mostly) opaque Cross-Origin
WindowProxy. Unless we split the Window Actors though, these two documents may stillBroadcastChanneleach-other aSharedArrayBufferobject, so we need to be explicit about putting the documents in distinct Window Actors.Notably, if the Toplevel Browsing Context's document is not isolated, we don't have opener hidden due to
window.top, for example:In this case, we can reach from
C-2toC-1usingwindow.top.opener, so we should not put them in distinct Window Agents.This brings us to the last restriction I think we have to make, namely:
Restricted Window Agent Selection Process
All of that is a touch tricky to follow. This is the final, combined, Window Agent selection process:
To select a global
A's Window Agent, consider each other globalB, and join its Window Agent if:AandBare in Related Browsing Contexts, andAandBare Same-Site, andAandBare either both isolated or both non-isolated, andAandBshare a Toplevel Browsing Context, orAandBare non-isolated and have non-isolated globals in both of their Toplevel Browsing Contexts.Implications
These are a few notable implications:
postMessage.Allowed to Navigate
As navigating by named lookups can also create cross-global references (due to the opener property), we also have to restrict the allowed to navigate check, adding 2 new checks:
A's orB's global is isolated, then:A's andB's global have different Window Agents, return falseAandBhave different Toplevel Browsing Contexts:A's orB's Toplevel Browsing Context's global is isolated, return falseThis should bring the checks in line with the Window Agent selection process.
Other Changes
This approach assumes that Window Agents are procedurally joined and used as the base of object access security. This is, however, not how the check is performed in the standard right now. We would want to change places which perform the Same Origin-Domain check for globals
AandBto instead check:AandBare Same Origin-Domain, andAandBare in the same Window AgentThis would change Window Agent selection to define access control, rather than being a result of it.
Restricting
document.domainIn the past I have been under the impression that we are interested in also restricting the use of
document.domainwithin isolated documents to make the Window Agents even smaller. I worry that doing this restriction could prevent some websites from switching to isolated documents, but it may be desirable for security.I'd be curious what people from Google and other websites think about this restriction.
rniwa commentedon Jun 28, 2018
@mystor Thanks for the detailed feedback. All your points are valid and we made the same observation. However, what we concluded is that even those isolations are not enough because websites inside iframes would have access to the same set of cookies, local storage, etc... unless we treat it as a separate origin. If websites have access to those resources, we don't have a meaningful Spectre protection since the most sensitive information is stored in cookies and other storage APIs.
For this reason, we believe that in order for websites to protect themselves against Spectre in a browser which only supports top-level process isolation MUST deny themselves loaded in an untrusted cross-origin iframe at all, and all of their resources must have
Cross-Origin-Window-Policyset to deny access.More precisely, we believe websites MUST:
Cross-Origin-Resource-Policyheader tosame-siteorsame-origin(See Cross-Origin-Resource-Policy (was: From-Origin) fetch#687)Cross-Origin-Window-Policyheader toDenyorAllow-PostMessage.X-Frame-Optionsheader toDENYorSAMEORIGINorCSP frame-ancestors.rniwa commentedon Jun 28, 2018
I'd also note that we've considered treating a website with
Cross-Origin-Window-Policyset as a different origin but this posed a number of new challenges like having to update all our internal tracking of origins to keep an extra bit and passing it around in terms of implementation challenges, and it's confusing for developers that now URL alone can't determine the origin of a document, etc... so we decided against it.mystor commentedon Jun 30, 2018
My worry here is that I don't think we can provide process isolation even if
web developers opt into all of the headers.
Consider the example where we have a window
A-1which opens a windowB-1.B-1follows all of the rules you've laid out above:If I understand this proposal correctly, in this situation we definitely want
A-1andB-1to be in separate processes before we have OOP iframes. I don'tthink we can do that, however.
For example,
A-1is capable of dynamically framing a new document,B-2fromsite
B. As we do not know the set of documents and their headers from siteB, it is theoretically possible forB-2to frame a document setting noneof the above headers, even if a site doesn't serve any documents without them.
If
A-1held a cross-origin visible reference toB-1's browsing context,(e.g. due to
openerbeing reversed, which could be arranged withoutB-1'scooperation through navigations,
window.open, etc.) then the theoreticaliframe
B-2would be able to get synchronous access toB-1's JS globalthrough
window.parent.opener, requiring them to be same-process.Even if we could prevent
A-1from obtaining a cross-origin visiblereference to
B-1's browsing context,B-2would be in the same AgentCluster as
B-1, which means that they could shareSharedArrayBufferobjects over a
BroadcastChannel, requiring them to be same-process.As we don't support OOP iframes, the theoretical
B-2must be in the sameprocess as
A-1. However,B-2must also be in the same process asB-1,forcing
B-1andA-1to be same-process.rniwa commentedon Jul 13, 2018
@mystor Thanks for the clarification. That's indeed an issue. There appears to be a number of possible solutions including but not limited to the one you proposed, checking all ancestor frames, clearing
window.openerof any window opened by isolated documents, checking the access totop.openerbased on whether it was opened by an isolated document, etc... We're having an internal discussion to either agree to the amendment you're proposing or further counter proposals.Meanwhile, we've disabled this feature in WebKit since this discussion is likely to result in an incompatible behavior change to the HTTP header we're proposing.
Again, thanks for giving us the detailed feedback & engaging in the discussion with us.
rniwa commentedon Aug 5, 2018
Just as an update, we're still discussing the best solution for this problem. Hoping to get back to you all within the next one month.
tomrittervg commentedon Aug 24, 2018
@rniwa There's a typo, AFAICT in the first paragraph that tripped me up. You say "the value case-insensitively matches Deny ignoring cases" twice - I think the second one is supposed to be 'Allow-PostMessage', right?
And another typo below: "Then b.com doesn't have access to a.com, and b.com doesn't have access to a.com so we can put them into two different processes." - the second b.com/a.com should be swapped I believe.
rniwa commentedon Aug 30, 2018
@tomrittervg Thanks for pointing that out. Fixed.
rniwa commentedon Aug 30, 2018
Okay, here's our feedback.
Our new proposal is to only support
AllowandDeny, and removeAllow-PostMessagefor now. When navigating in a window opened from or navigating to a fully isolated document (i.e. either the opener or the destination hasDenyset) then, we would clearwindow.openerand create a new unit of related browsing context. This would mean thatBroadcastChannelwould not be delivered to those pages.While it's regrettable that web pages that require
postMessagewon't be able to adopt this header in near future with this approach, we concluded this is the least problematic resolution of all.As I've previously stated, treating isolated and non-isolated pages as more or less different origins would make it harder to incrementally adopt this header across a website.
We've analyzed the proposal made by @mystor in detail but we could resolve two issues:
We've also considered keeping
Allow-PostMessageand only providing Spectre protection forDenyand not onAllow-PostMessage(i.e. browsers that don't implement frame-level process isolation would keep them in the same process) but we're worried that the message for developers will be tooconfusing and misleading.
60 remaining items
arturjanc commentedon Jan 29, 2019
Would the network error for sandbox documents only apply to top-level contexts, both non-auxiliary and auxiliary? If so, this is probably okay as they are indeed a fairly niche case -- otherwise, if we also network error'ed sandbox frames, it could be a larger obstacle to adoption, e.g. if the site has ads.
FWIW there are scenarios where maliciously putting a cross-origin document in the sandbox can be used in attacks (e.g. open a document from victim origin in a sandbox window which doesn't allow modals to prevent an alert() from warning the user about some unexpected condition). So the "easy option" of responding with a network error here might actually be security-positive.
annevk commentedon Jan 29, 2019
Cross-Origin-Opener-Policy only takes effect in top-level, so yes. For nested contexts you could use X-Frame-Options. If you think further exploring dedicated anti-sandbox features are worth it, let's do that in a separate issue (it seems reasonable to me by the way, I was somewhat surprised we never really considered this before)?
annevk commentedon Apr 24, 2019
A thing we have not discussed in detail here, though @mystor did mention it in #3740 (comment) is history.
Firefox's current approach is history is copied somewhat into the new browsing context group, so
history.back()et al do function to some extent. My personal feeling is that the UX for history should continue to work, but API-wise it should feel as if the user navigated directly to your (COOP) document.What are Chrome and Safari considering here?
csreis commentedon Apr 24, 2019
For Chrome, we're keeping the session history intact across browsing context groups, both for the back/forward buttons and history.back() et al. Chrome manages the session history for a tab in the browser (parent) process, with a notion of browsing context group (BrowsingInstance) and process (SiteInstance) associated with each session history item.
arturjanc commentedon Jun 9, 2019
I think the desired behavior here is what @annevk described in #3740 (comment): we want history navigations via the browser UI to work, but ideally they wouldn't be accessible via
window.history.A risk with allowing access to history from JS is that upon a navigation from a site with COOP to an attacker's document the browser may leak interesting information (e.g. an attacker can get information about the number of navigations on the victim site via
window.history.length).If it adds significant implementation / spec difficulty then the severity of the leak is likely not worth addressing, but there are some benefits to removing JS access to history in this case.
annevk commentedon Dec 5, 2019
As a result of some related issues, in particular how inheritance ought to work and whether
unsafe-inheritwould be sufficient if an OAuth-chain had cross-site documents in it as well, we ended up simplifyingCross-Origin-Opener-Policya bit. It now has an explicit default,unsafe-none, which is mostly there to potentially allow for changing the default at a future point (and allow sites to opt out of such a change). And the remaining values aresame-originandsame-origin-allow-popups. Given the problems withunsafe-inheritthere seemed to be less of a need forsame-site-like policies, but adding those back in would not be a lot of work if developers request them.https://gist.github.com/annevk/6f2dd8c79c77123f39797f6bdac43f3e is still the canonical document and I've updated it with these changes.
Add the Cross-Origin-Opener-Policy header
Add the Cross-Origin-Opener-Policy header
Add the Cross-Origin-Opener-Policy header
Add the Cross-Origin-Opener-Policy header
Add cross-origin opener policy
JoeMarkov commentedon Dec 19, 2020
The Cross-Origin-Opener-Policy header seems to be quite similar to what the rel="noopener noreferrer" attribute does when opening a new document in a new tab (target="_blank").
When should I use which one? It seems the COOP header is applicable when I link between origins while the rel="noopener noreferrer" attribute (on anchor tags) seems to work on the same origin as well?
Should I use both? They seem to be quite complimentary.... Some advice here would be nice.
Add cross-origin opener policy