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

Closed
rniwa opened this issue Jun 5, 2018 · 76 comments
Closed
Labels
addition/proposal New features or enhancements impacts documentation Used by documentation communities, such as MDN, to track changes that impact documentation security/privacy There are security or privacy implications topic: browsing context topic: cross-origin-opener-policy Issues and ideas around the new "inverse of rel=noopener" header.

Comments

@rniwa
Copy link
Collaborator

rniwa 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 domenic added the security/privacy There are security or privacy implications label Jun 5, 2018
@domenic
Copy link
Member

domenic commented Jun 5, 2018

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

@bzbarsky
Copy link
Contributor

bzbarsky 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
Copy link
Collaborator Author

rniwa commented Jun 5, 2018

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

@mystor
Copy link
Contributor

mystor 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
Copy link
Collaborator Author

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

@rniwa
Copy link
Collaborator Author

rniwa 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
Copy link
Contributor

mystor 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
Copy link
Collaborator Author

rniwa 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
Copy link
Collaborator Author

rniwa 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
Copy link

tomrittervg 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
Copy link
Collaborator Author

rniwa commented Aug 30, 2018

@tomrittervg Thanks for pointing that out. Fixed.

@rniwa
Copy link
Collaborator Author

rniwa 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
Copy link
Contributor

mystor 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
Copy link
Collaborator Author

rniwa 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
Copy link
Member

annevk 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
Copy link
Collaborator Author

rniwa 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
Copy link

csreis 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
Copy link
Collaborator Author

rniwa 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
Copy link
Contributor

mystor 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
Copy link
Member

annevk 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
Copy link

csreis 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
Copy link

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
Copy link
Member

annevk commented Dec 5, 2019

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.

annevk added a commit to camillelamy/html that referenced this issue Jun 5, 2020
Fixes whatwg#3740. Closes whatwg#4580.

Need to check again if it closes them:

* whatwg#4921
* whatwg#5168
* whatwg#5172
* whatwg#5198 (probably not?)

Co-authored-by: Anne van Kesteren <annevk@annevk.nl>
camillelamy pushed a commit to camillelamy/html that referenced this issue Jun 10, 2020
Fixes whatwg#3740. Closes whatwg#4580.

Need to check again if it closes them:

* whatwg#4921
* whatwg#5168
* whatwg#5172
* whatwg#5198 (probably not?)

Co-authored-by: Anne van Kesteren <annevk@annevk.nl>
camillelamy pushed a commit to camillelamy/html that referenced this issue Jun 16, 2020
Fixes whatwg#3740. Closes whatwg#4580.

Need to check again if it closes them:

* whatwg#4921
* whatwg#5168
* whatwg#5172
* whatwg#5198 (probably not?)

Co-authored-by: Anne van Kesteren <annevk@annevk.nl>
camillelamy pushed a commit to camillelamy/html that referenced this issue Jun 24, 2020
Fixes whatwg#3740. Closes whatwg#4580.

Need to check again if it closes them:

* whatwg#4921
* whatwg#5168
* whatwg#5172
* whatwg#5198 (probably not?)

Co-authored-by: Anne van Kesteren <annevk@annevk.nl>
mfreed7 pushed a commit to mfreed7/html that referenced this issue Sep 11, 2020
This commit adds the notion of cross-origin opener policy (COOP). COOP
allows websites to restrict which origins they share their browsing
context group with. annevk wrote a first draft of the behavior of COOP
here: https://gist.github.com/annevk/6f2dd8c79c77123f39797f6bdac43f3e.
This takes that draft and merges it into the spec, with many updates
along the way.

Closes whatwg#3740. Closes whatwg#4580. Closes whatwg#4921. Closes whatwg#5172.

Co-authored-by: clamy <clamy@chromium.org>
Co-authored-by: Anne van Kesteren <annevk@annevk.nl>
Co-authored-by: Domenic Denicola <d@domenic.me>
@JoeMarkov
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements impacts documentation Used by documentation communities, such as MDN, to track changes that impact documentation security/privacy There are security or privacy implications topic: browsing context topic: cross-origin-opener-policy Issues and ideas around the new "inverse of rel=noopener" header.
Development

No branches or pull requests

10 participants