Skip to content

Restricting cross-origin WindowProxy access (Cross-Origin-Opener-Policy) #3740

Closed

Description

@rniwa

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:

  1. 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 a SecurityError`.

  2. If document B is isolated with messaging and document A is not isolated. Any attempt to access a property except postMessage on document B's window from document A results in a SecurityError. Any attempt to access a property except postMessage on document A's window from document B results in a SecurityError.

  3. 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 a SecurityError.

  4. If document B is isolated with messaging and document A is isolated with messaging. Any attempt to access a property except postMessage on document B's window from document A results in a SecurityError. Any attempt to access a property except postMessage on document A's window from document B results in a SecurityError.

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

  1. domenic commented on Jun 5, 2018

    @domenic
    Member

    /cc @whatwg/security. Very interesting...

  2. bzbarsky commented on Jun 5, 2018

    @bzbarsky
    Contributor

    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.

  3. rniwa commented on Jun 5, 2018

    @rniwa
    Author

    Note that this feature has been implemented in Safari 12: https://developer.apple.com/safari/whats-new/

  4. mystor commented on Jun 21, 2018

    @mystor
    Contributor

    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 WindowProxy and Location objects). A Window Agent is in an Agent Cluster which also includes dedicated workers. Currently, two globals
    have 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 over BroadcastChannel), and share a single event loop.

    Isolated/Non-Isolated Interaction

    +-----------+           +-----------+
    |     A     | -opener-> |  B-1 (I)  |
    +-----------+           +-----------+
    
    (I): Isolated
    

    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:

    +-----------+           +-----------+
    |     A     | -opener-> |  B-1 (I)  |
    |           |           +-----------+
    |+---------+|
    ||   B-2   ||
    |+---------+|
    +-----------+
    
    (I): Isolated
    

    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:

    // In B-2
    let b1 = window.parent.opener;
    // b1 is same-origin-document with window, so we have to be same-process
    b1.document // :'-(

    This brings us to the first restriction I think we have to make, namely:

    An isolated global may only share its Window Agent with another isolated global

    Isolated/Isolated Interaction

    Unfortunately, that restriction is not sufficient. If window.opener references 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.open to open a new Toplevel Browsing Context containing an isolated document A:

                           +-----------+
                           |     B     |
    +-----------+          |+---------+|
    |  A-1 (I)  | -opener-> |    C    ||
    +-----------+          |+---------+|
                           +-----------+
    

    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-Domain to A-1:

                           +-----------+
                           |     B     |
    +-----------+          |+---------+|
    |  A-1 (I)  | -opener-> | A-2 (I) ||
    +-----------+          |+---------+|
                           +-----------+
    

    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:

    An isolated global may only share its Window Agent with another global with the same Toplevel Browsing Context

    Shared Array Buffer

    With those two restrictions, things are looking pretty good. Unfortunately, SharedArrayBuffer strikes again. SharedArrayBuffer can be sent over BroadcastChannel, 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:

    +-----------+           +-----------+
    |   A (I)   | -opener-> |    B-1    |
    |+---------+|           +-----------+
    ||   B-2   ||
    |+---------+|
    +-----------+
    

    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 still BroadcastChannel each-other a SharedArrayBuffer object, 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:

    +-----------+           +-----------+
    |     A     | -opener-> |    C-1    |
    |+---------+|           +-----------+
    ||  B (I)  ||
    ||+-------+||
    |||  C-2  |||
    ||+-------+||
    |+---------+|
    +-----------+
    

    In this case, we can reach from C-2 to C-1 using window.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:

    A non-isolated global may only share Window Agent with another global with the same Toplevel Browsing Context, or, if both globals have a non-isolated global in their Toplevel Browsing Context.

    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 global B, and join its Window Agent if:

    1. A and B are in Related Browsing Contexts, and
    2. A and B are Same-Site, and
    3. A and B are either both isolated or both non-isolated, and
    4. One of:
      1. A and B share a Toplevel Browsing Context, or
      2. A and B are non-isolated and have non-isolated globals in both of their Toplevel Browsing Contexts.

    Implications

    These are a few notable implications:

    1. Isolated globals will not join the same Window Agent, and thus cannot share objects with, any global in another tab or window
      1. Isolated documents cannot share objects with pop-up windows, and may only communicate through postMessage.
      2. This may limit some websites from performing isolation, but this is probably unavoidable.
    2. A non-isolated document is in the same Window Agent as every non-isolated, Same-Site, browsing context which it can obtain a reference to.
      1. This is nice for maintaining the predictability of the system.

    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:

    1. If either A's or B's global is isolated, then:
      1. If A's and B's global have different Window Agents, return false
    2. If A and B have different Toplevel Browsing Contexts:
      1. If either A's or B's Toplevel Browsing Context's global is isolated, return false

    This 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 A and B to instead check:

    1. The origins of A and B are Same Origin-Domain, and
    2. A and B are in the same Window Agent

    This would change Window Agent selection to define access control, rather than being a result of it.

    Restricting document.domain

    In the past I have been under the impression that we are interested in also restricting the use of document.domain within 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.

  5. rniwa commented on Jun 28, 2018

    @rniwa
    Author

    @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-Policy set to deny access.

    More precisely, we believe websites MUST:

    1. Set Cross-Origin-Resource-Policy header to same-site or same-origin (See Cross-Origin-Resource-Policy (was: From-Origin) fetch#687)
    2. Set Cross-Origin-Window-Policy header to Deny or Allow-PostMessage.
    3. Set X-Frame-Options header to DENY or SAMEORIGIN or CSP frame-ancestors.
  6. rniwa commented on Jun 28, 2018

    @rniwa
    Author

    I'd also note that we've considered treating a website with Cross-Origin-Window-Policy set 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.

  7. mystor commented on Jun 30, 2018

    @mystor
    Contributor

    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-1 which opens a window B-1.
    B-1 follows all of the rules you've laid out above:

    Cross-Origin-Resource-Policy: same-origin
    Cross-Origin-Window-Policy: Deny
    X-Frame-Options: DENY
    
    +---------+           +---------+
    |   A-1   | <-opener- | B-1 (I) |
    +---------+           +---------+
    

    If I understand this proposal correctly, in this situation we definitely want
    A-1 and B-1 to be in separate processes before we have OOP iframes. I don't
    think we can do that, however.

    For example, A-1 is capable of dynamically framing a new document, B-2 from
    site B. As we do not know the set of documents and their headers from site
    B, it is theoretically possible for B-2 to frame a document setting none
    of the above headers, even if a site doesn't serve any documents without them.

    1. If A-1 held a cross-origin visible reference to B-1's browsing context,
      (e.g. due to opener being reversed, which could be arranged without B-1's
      cooperation through navigations, window.open, etc.) then the theoretical
      iframe B-2 would be able to get synchronous access to B-1's JS global
      through window.parent.opener, requiring them to be same-process.

    2. Even if we could prevent A-1 from obtaining a cross-origin visible
      reference to B-1's browsing context, B-2 would be in the same Agent
      Cluster as B-1, which means that they could share SharedArrayBuffer
      objects over a BroadcastChannel, requiring them to be same-process.

    As we don't support OOP iframes, the theoretical B-2 must be in the same
    process as A-1. However, B-2 must also be in the same process as B-1,
    forcing B-1 and A-1 to be same-process.

  8. rniwa commented on Jul 13, 2018

    @rniwa
    Author

    @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.opener of any window opened by isolated documents, checking the access to top.opener based 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.

  9. rniwa commented on Aug 5, 2018

    @rniwa
    Author

    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.

  10. tomrittervg commented on Aug 24, 2018

    @tomrittervg

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

  11. rniwa commented on Aug 30, 2018

    @rniwa
    Author

    @tomrittervg Thanks for pointing that out. Fixed.

  12. rniwa commented on Aug 30, 2018

    @rniwa
    Author

    Okay, here's our feedback.

    Our new proposal is to only support Allow and Deny, and remove Allow-PostMessage for now. When navigating in a window opened from or navigating to a fully isolated document (i.e. either the opener or the destination has Deny set) then, we would clear window.opener and create a new unit of related browsing context. This would mean that BroadcastChannel would not be delivered to those pages.

    While it's regrettable that web pages that require postMessage won'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:

    • Doing top-level browsing context check even in the same origin case would incur a serious performance cost in accessing any global object's property.
    • It's a weird new concept that web developers need to understand. There is no other precedent in making a top-level browsing context special like this.

    We've also considered keeping Allow-PostMessage and only providing Spectre protection for Deny and not on Allow-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 too
    confusing and misleading.

  13. 60 remaining items

  14. arturjanc commented on Jan 29, 2019

    @arturjanc

    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.

  15. annevk commented on Jan 29, 2019

    @annevk
    Member

    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)?

  16. annevk commented on Apr 24, 2019

    @annevk
    Member

    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?

  17. csreis commented on Apr 24, 2019

    @csreis

    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?

    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.

  18. arturjanc commented on Jun 9, 2019

    @arturjanc

    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.

  19. annevk commented on Dec 5, 2019

    @annevk
    Member

    As a result of some related issues, in particular how inheritance ought to work and whether unsafe-inherit would be sufficient if an OAuth-chain had cross-site documents in it as well, we ended up simplifying Cross-Origin-Opener-Policy a 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 are same-origin and same-origin-allow-popups. Given the problems with unsafe-inherit there seemed to be less of a need for same-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.

  20. added a commit that references this issue on Jun 5, 2020
    d14fa78
  21. added 2 commits that reference this issue on Jun 10, 2020
    5915ea0
    a31a802
  22. added a commit that references this issue on Jun 24, 2020
    06b4d8a
  23. added a commit that references this issue on Sep 11, 2020
    e950d4b
  24. JoeMarkov commented on Dec 19, 2020

    @JoeMarkov

    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.

  25. added a commit that references this issue on May 18, 2025
    c9fddd7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions