Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
rniwa opened this issue Jun 5, 2018 · 74 comments

Comments

@rniwa
Copy link
Collaborator

commented Jun 5, 2018

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.

@domenic

This comment has been minimized.

Copy link
Member

commented Jun 5, 2018

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

@bzbarsky

This comment has been minimized.

Copy link
Collaborator

commented 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

This comment has been minimized.

Copy link
Collaborator Author

commented Jun 5, 2018

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

@mystor

This comment has been minimized.

Copy link
Contributor

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

@rniwa

This comment has been minimized.

Copy link
Collaborator Author

commented 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-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 whatwg/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.
@rniwa

This comment has been minimized.

Copy link
Collaborator Author

commented Jun 28, 2018

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.

@mystor

This comment has been minimized.

Copy link
Contributor

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

@rniwa

This comment has been minimized.

Copy link
Collaborator Author

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

@rniwa

This comment has been minimized.

Copy link
Collaborator Author

commented 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

This comment has been minimized.

Copy link

commented 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

This comment has been minimized.

Copy link
Collaborator Author

commented Aug 30, 2018

@tomrittervg Thanks for pointing that out. Fixed.

@rniwa

This comment has been minimized.

Copy link
Collaborator Author

commented Aug 30, 2018

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.

@mystor

This comment has been minimized.

Copy link
Contributor

commented Aug 31, 2018

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.

Just wanting to make sure I have a clear idea of what your proposal here is, so I've done a slightly more technical writeup of what I am thinking you are suggesting. Please let me know if I am off-base.

I think I like this idea, it's fairly simple, but is unfortunately destructive (breaks all WindowProxy references in perpetuity), and doesn't support PostMessage (though I have a potential ugly workaround at the bottom).

While loading a Document

The following steps would be taken when loading a new document doc into a browsing context context, with existing document oldDoc (the initial about:blank document is unisolated):

  1. If neither of oldDoc and doc are isolated, skip the following steps

    NOTE: This is the current case of unisolated => unisolated loads.

  2. If oldDoc and doc are both isolated, and same-origin, skip the following steps

    NOTE: Allow loads from isolated(foo.com) => isolated(foo.com) to work as-today.

  3. If context is a toplevel browsing context, perform the following steps

    NOTE: This occurs in a few different cases:

    • isolated(foo.com) => isolated(bar.com) - we want to protect both, so make a new context
    • isolated(foo.com) => unisolated - protect the isolated document from unisolated document being loaded
    • unisolated => isolated(foo.com) - protect the isolated document from previously loaded documents
    1. Create a new toplevel browsing context newContext in a new window agent
    2. Transplant session history information from context into newContext
    3. Copy session storage & cookie information from context into newContext
    4. Move the current load of doc into newContext
    5. Visually replace context with newContext in the browser ui
    6. Close context
Protections
  1. Any existing references to your window will be broken upon loading a toplevel isolated document.
  2. A toplevel document loaded this way could be guaranteed a separate process from all other tabs without browser support for cross-process window proxies or out of process iframes.
Non-Protections
  1. The isolated document may frame any document, and has full normal access to that document. These subframes would be able to attack the isolated document

    Sites should be careful with which documents they frame, potentially using CSP for added control

  2. If the isolated document is framed by an attacker, it will not be isolated

    Sites must use X-Frame-Options to protect themselves

  3. Auxiliary browsing contexts created by the isolated document or its subframes are not forced into a separate process

    Sites must create auxiliary browsing contexts with noopener to protect themselves

  4. Documents with this header may be loaded by cors-exempt subresource loads

    This will be mitigated by CORB and other strategies

Side-Effects
  1. Navigating to previously loaded pages will load them in a newly created browsing context, rather than the context they were originally loaded in.
  2. Existing postMessage edges are not supported.

Potential Variations

  1. The initial about:blank document, doc, could be treated more specially. Specifically:

    This would allow isolated documents to open same-origin isolated auxiliary browsing contexts without breaking window references, while also continuing to allow unisolated auxiliary browsing contexts etc.
    However, this could make for a larger footgun compared to the above model, and is more complex.

    1. If doc has an isolated original opener document opener, and the to-be-loaded document is isolated, doc is the same as opener
    2. Otherwise, doc is unisolated

postMessage

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.

This solution should be relatively straightforward to Firefox to implement, and it manages to avoid breaking all postMessage edges, but it probably can't be served on oauth popups.

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.

I think I generally agree with this. I tried to think of how we could support postMessage within this framework, and came up with the following solution, which is unfortunately mildly compex/gross:

  1. if Allow-PostMessage is set, all WindowProxy references to the original browsing context, context, have the internal [forwardPostMessage] property set to newContext
  2. When calling the Window::postMessage method where this is a WindowProxy to a closed window, check [forwardPostMessage]. If it is set, perform the following steps:
    1. Let source be the calling context
    2. Let target be the context stored in [forwardPostMessage]
    3. Serialize message using the structured clone algorithm.
    4. If target's origin does not match targetOrigin, abort these steps
    5. Create a MessageEvent event with the data being the structured clone of message, origin being the origin of source, and source being a MessageEventSource object with a postMessage method, which can be called to perform these steps, but in the other direction
    6. Dispatch event on the current global of target

Effectively, this allows postMessage by keeping the method alive on closed WindowProxies created through the mechanism of this header, and using a dummy MessageEventSource method in the dispatched MessageEvent.

@rniwa

This comment has been minimized.

Copy link
Collaborator Author

commented Sep 11, 2018

Non-Protections

  1. The isolated document may frame any document, and has full normal access to that document. These subframes would be able to attack the isolated document
    Sites should be careful with which documents they frame, potentially using CSP for added control

Are you describing a same origin document or a cross-origin document? In the case of a cross-origin document, the proposal calls for zero access. In the case of a same-origin document, yes, the proposal allows access, but no protection is needed.

  1. If the isolated document is framed by an attacker, it will not be isolated
    Sites must use X-Frame-Options to protect themselves

Yes, to ensure process isolation you must specify X-Frame-Options or CSP's frame-ancestors.

That said, the isolated document is still isolated in the sense that access to its window proxy properties is denied across origins.

  1. Auxiliary browsing contexts created by the isolated document or its subframes are not forced into a separate process. Sites must create auxiliary browsing contexts with noopener to protect themselves.

A fresh auxiliary browsing context on the same origin will not be forced into a separate unit of related browsing contexts. But navigation to a different origin will trigger isolation. Isolation includes clearing the opener property. This allows a process swap and eliminates the need to specify noopener separately.

  1. Documents with this header may be loaded by cors-exempt subresource loads
    This will be mitigated by CORB and other strategies

Right.

Side-Effects

  1. Navigating to previously loaded pages will load them in a newly created browsing context, rather than the context they were originally loaded in.

We don’t think this is mandated, but it is an option. We could go either way in the specification. Restoring the previous browsing context seems more web compatible.

  1. Existing postMessage edges are not supported.

Right.

Potential Variations.

  1. The initial about:blank document, doc, could be treated more specially. Specifically: This would allow isolated documents to open same-origin isolated auxiliary browsing contexts without breaking window references, while also continuing to allow unisolated auxiliary browsing contexts etc. However, this could make for a larger footgun compared to the above model, and is more complex.

We believe that about:blank has the origin of its opener. So we don’t understand the issue here. Maybe we're using the wrong terminology here?

@annevk

This comment has been minimized.

Copy link
Member

commented Sep 28, 2018

I had a chat about an alternative model with @mystor.

Ingredients:

  • A new header / feature policy / CSP thingy called "Isolate" here that a) specifies same-origin (same) or same-site and b) optionally specifies a strict flag
  • A browsing context has a new isolate member that's either null or a struct consisting of a) an origin, b) same-origin or same-site, and c) strict (boolean)

When creating an auxiliary/nested browsing context:

  • Copy the isolate member from the "opening" browsing context.

When navigating:

  1. If this is a nested browsing context and its isolate is non-null, strict of its isolate is true, and the navigation crosses the isolate's origin coupled with same-origin/same-site boundary, then return a network error.

  2. If one of the following is true

    • this is an auxiliary browsing context, its isolate is non-null, and the strict of that is true
    • this is a top-level browsing context and its isolate is non-null

    and the navigation crosses the isolate's origin coupled with same-origin/same-site boundary, then create a new top-level browsing context to handle the navigation and close the currently navigated browsing context. (This severs all connections.)

  3. If the response contains an (valid) "Isolate" and the "Isolate" does not match browsing context's isolate, then create a new browsing context to handle the navigation and close the currently navigated browsing context. (This also happens for nested browsing contexts. Severs all connections.)

Tradeoffs:

  • We could make auxiliary browsing contexts also return a network error in strict mode for consistency with nested browsing contexts, but it's not clear how that's more useful than the proposed behavior.

Goals:

  • If the top-level browsing context is strict (however sites manage to get there, e.g., using X-Frame-Options in combination with "Isolate") it should allow for enabling SharedArrayBuffer in browsers that have opted not to enable it by default for now.
  • Treat nested browsing contexts and auxiliary browsing contexts in an equivalent manner to the extent that's doable.

UI:

  • We should perhaps also encourage user agents to replace the browsing context with a new one when the user initiates the navigation through a bookmark or address bar.
@rniwa

This comment has been minimized.

Copy link
Collaborator Author

commented Oct 2, 2018

Per the last F2F discussion we had, we'd like to keep the level 1 proposal / feature to be focused on providing a mechanism for websites to protect themselves from Spectre attacks. That is, providing a way to enable SAB would be a good addition to this feature but shouldn't be a part / requirement of it. Also see Artur's summary in the isolation-policy mailing list.

Here's our latest proposal to that end.

Proposal

We introduce a new HTTP header, let us call it Cross-Origin-Opener-Policy for the sake of this description. The name can be changed to whatever we like. When navigating to a document with this header set in an auxiliary browsing context or any other top-level browsing context, the auxiliary browsing context is closed. The navigation instead occurs in a new browsing context created with its own unit of related browsing context.

Conceptually, this is as if the user had closed the tab/window in which the navigation was happening, and opened a new tab/window and navigated to the same page.

There is no restriction on which website a document with the HTTP header can open in a new window or load in an iframe.

Discussion

Various discussions we've had on this issue and elsewhere made us realize that the key issue with the process swap on navigation (PSON) in browsers that don't support frame-level process isolation is window.opener. Specifically, a nested browsing context (iframe) inside a cross-origin auxiliary browsing context can access the isolated document via top.opener. So we must sever this connection.

To enable a website to protect itself, the user agent doesn't need to restrict the website’s ability to load cross-origin content in an iframe or a new window. In fact, that might be a necessary condition for some websites to adopt this new protection header. Imagine a banking website which opens another financial institution's website (e.g. credit card company's) in a new window in order to process a certain transaction. In such a case, it's probably okay for the websites to trust one another and be opened in a single process. In fact, such a flexibility might be a requirement for a website to incrementally adopt this new header.

Making those observations, the only restriction we need for websites to protect themselves from Spectre attacks in a browser which doesn't support frame-level process isolation is that another website can't open it in an auxiliary browsing context. Note the website MUST prevent others from loading inside an iframe of a cross-origin site from the supposition.

Furthermore, it seemed to us that the ability to protecting your own website from the opener browsing context from navigating, etc... seems like a useful feature regardless of Spectre.

A Path to Level Two Protection

To re-iterate our position during F2F, we think it's valuable to provide a mechanism whereby browsers without a frame-level process isolation can re-enable SharedArrayBuffer provided the same feature is independently useful in some other context.

One way to achieve the level 2 protection is to provide a way to prevent all descendent browsing context from loading a cross-origin content just like CSP's frame-ancestors allows websites to prevent cross-origin websites to load itself.

When the browser sees that a website has this second level protection set at the top-level browsing context, then it can enable SharedArrayBuffer because such a browsing context cannot have, in its unit of related browsing context, any browsing context of cross-origin.

One possible appraoch

We would have two values for the header: deny-incoming and deny-all. deny-incoming is level one protection: your opener gets severed, but you can open whatever you want. deny-all is level two protection: you get deny-incoming and you also sever cross-origin windows you open. (As with the rest of the proposal, the names can change once we agree on the behavior.)

Then the level two protection can be achieved by the combination of deny-all and csp: frame-src self at the top-level browsing context.

@csreis

This comment has been minimized.

Copy link

commented Oct 2, 2018

Thanks for writing that up.

For "level 2" protection, is there any chance of including something like Mozilla's "X-Bikeshed-Allow-Unisolated-Embed" header (from the X-Bikeshed-Force-Isolate proposal) to allow web sites to opt in to being included in the same page and process as a site with SharedArrayBuffer access? Your description rules out all cross-origin browsing contexts, but that would (for example) prevent sites with SharedArrayBuffers from using ads, etc. I would imagine that ad sites might be ok with being embedded in such pages.

@rniwa

This comment has been minimized.

Copy link
Collaborator Author

commented Oct 3, 2018

@csreis : Perhaps. The level two protection proposal outlined in my latest comment is simply a possibility / an option. We're not necessarily fixated on it.

Having said that, how can something like X-Bikeshed-Allow-Unisolated-Embed be useful outside the very limited context of allowing SharedArrayBuffer to exist in a website which loads cross-origin ads in a browser which doesn't support frame-level process isolation?

Again, our feedback / desire for the level two protection is that it ought to be something useful outside the context of Spectre and SharedArrayBuffer.

@mystor

This comment has been minimized.

Copy link
Contributor

commented Oct 3, 2018

Thanks for writing this up @rniwa!

I believe we will need to perform opener severing both for auxiliary and toplevel browsing contexts. If we only handle auxiliary browsing contexts, an attacker can open a new tab in the background, and then navigate itself to your toplevel page, attacking your document due to the opener property on the background tab.

I think a way to make X-Bikeshed-Allow-Unisolated-Embed useful beyond spectre would be for it to become part of a system of mutual consent. Namely, a level-2 isolated document both needs to consent to loading a nested frame, and the nested frame needs to consent to being loaded. This might be nice for websites wanting to protect themselves through better hygiene in general.

@annevk

This comment has been minimized.

Copy link
Member

commented Dec 12, 2018

Cases that I don't think we explicitly discussed:

  • A embeds B, B sets this header. I think we decided that B's browsing context doesn't get replaced (we only replace a browsing context group). But what do we do for popups assuming unsafe-allow-outgoing isn't set? It makes some sense to me that popups would end up as new browsing context groups.
  • A embeds B, A sets this header. What happens for popups that B opens? I can see arguments either way.
  • The above two scenarios, but with A embedding A' (same-origin, different document).
@arturjanc

This comment has been minimized.

Copy link

commented Dec 17, 2018

I don't have strong feelings either way, so my guess is that we should aim for conceptually the simplest behavior. To me, it would make sense if the header was always ignored in a nested browsing context (so B would have the same behavior regardless of whether it sets the header). When B opens a pop-up, it seems reasonable for it to obey the unsafe-allow-outgoing value set on A -- this introduces a minor leak (A can know if B opened a new window), but this doesn't seem much worse than the status quo. Just my two cents, though...

One other thing that came up earlier but that AFAIK we haven't fully resolved is the Upgrade-No-CORS header and whether to automatically supply credentials on upgraded requests. I think we'd need to do it to not require significant refactoring of code/markup which loads authenticated subresources; but this behavior could be a little surprising, so we should probably put some thought into this.

@annevk

This comment has been minimized.

Copy link
Member

commented Jan 4, 2019

Okay, so tentatively for Cross-Origin-Opener-Policy:

  1. Only works when declared on a resource navigated to in a top-level browsing context (this includes auxiliary which will become non-auxiliary due to this header except if there's a match).
  2. Sets a policy for the browsing context group (and therefore "inherits" into nested and any auxiliary browsing contexts).
  3. A match between a browsing context group and a new top-level browsing context resource that is navigated to therein requires an identical sameness and unsafe-allow-outgoing. A non-match results in a new browsing context group (within which a new top-level browsing context is created for the non-matching resource).

Let's discuss CORS in #4175. Mozilla still intends to ship both together, but given the reservations stated in the meeting it might be good to somewhat separate the discussions for now.

@annevk

This comment has been minimized.

Copy link
Member

commented Jan 4, 2019

(I didn't realize this originally covered CORS as well, my bad. I think it would make sense to keep this for Cross-Origin-Opener-Policy and the other issue for enabling SharedArrayBuffer.)

@annevk

This comment has been minimized.

Copy link
Member

commented Jan 8, 2019

Having written up a more formal description for COOP I realize I made some minor errors above. In particular there's no need for this policy to be on the browsing context group. Either auxiliary browsing contexts are allowed (if the unsafe flag is set) or they create new browsing context groups. So we only need changes to navigating a top-level browsing context (includes auxiliary, to be clear) and creating an auxiliary browsing context (might have to create a new browsing context group instead). Hope this new description aides in review.

@annevk annevk changed the title Need a header to restrict cross-origin window proxy access Restricting cross-origin WindowProxy access (Cross-Origin-Opener-Policy) Jan 18, 2019

@annevk

This comment has been minimized.

Copy link
Member

commented Jan 18, 2019

I updated the description for COOP linked above with initial about:blank document handling and redirect handling. I think from a HTML Standard perspective we could also make "fast back" work by allowing browsing contexts to be closed/opened in addition to being discarded. We'd close it when replacing it with BCG and open it when going back. I'm not sure if this is worth specifying for v1 (although maybe it turns out to be easier), but as "fast back" is optional anyway I don't think it matters much.

The way the about:blank handling for unsafe-allow-outgoing should work out such that a navigation from initial about:blank ends up with replacement if there's a non-matching COOP on the response. I.e., if there's no COOP on the response, no replacement happens and you get an unsafe reference. If it's a "sameness" navigation and the response has a matching COOP, no replacement happens and you get a reference as well, because about:blank inherits COOP from its creator. A further navigation from that sameness response with COOP to a non-matching COOP would yield replacement. That seems like the right model since otherwise setting the header would sometimes not give the isolating effect it promises.

@annevk

This comment has been minimized.

Copy link
Member

commented Jan 29, 2019

While working on #4284 I realized that non-auxiliary top-level browsing contexts can also get assigned external state:

  • A name, even when noopener is used. I think if Cross-Origin-Opener-Policy is specified we should get rid of the name. This has no negative consequences that I can think of. The site itself can still set it.
  • Sandboxing flags. We cannot get rid of these as it would allow for escaping the sandbox. So either we allow and apply them, or we "network error" the browsing context. The scenario to get here is A being sandboxed with "allow-popups" and opening B. If B has Cross-Origin-Opener-Policy set we run into this. If A used noopener we also run into this and in that case we copy over the flags. So we'll have to support creating a new browsing context group with some inherited flags, but it would be nice if Cross-Origin-Opener-Policy meant no inherited state, so I'd propose we network error here given that it's a nice cache.

(Apologies for originally posting these observations in the wrong issue. Thanks @csreis for the correction. I've marked the corresponding comments in the other issue as off-topic.)

@arturjanc

This comment has been minimized.

Copy link

commented 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

This comment has been minimized.

Copy link
Member

commented 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

This comment has been minimized.

Copy link
Member

commented 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

This comment has been minimized.

Copy link

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

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

This comment has been minimized.

Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.