whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
8.11k stars 2.67k forks source link

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

Closed rniwa closed 4 years ago

rniwa commented 6 years ago

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 aSecurityError`.

  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 commented 6 years ago

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

bzbarsky commented 6 years ago

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 commented 6 years ago

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

mystor commented 6 years ago

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 commented 6 years ago

@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 https://github.com/whatwg/fetch/issues/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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

@tomrittervg Thanks for pointing that out. Fixed.

rniwa commented 6 years ago

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:

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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

Ingredients:

When creating an auxiliary/nested 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:

Goals:

UI:

rniwa commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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.

mystor commented 6 years ago

One more thing - I don't believe we can re-use frame-src 'self' as it isn't inherited at the moment by subframes. I've thrown together a quick little demo page: https://yielding-bobolink.glitch.me/

screenshot from 2018-10-03 11-24-13

csreis commented 6 years ago

@rniwa: Ah, that's a different question if browsers with frame-level process isolation are allowed to load cross-origin browsing contexts in your level 2 proposal. That should be safe from the Spectre perspective, but it's functionally different than browsers without frame-level process isolation would behave.

On the plus side, it avoids the need for another header. However, it means that ads (etc) wouldn't be supported on sites with SharedArrayBuffers in most browsers until/unless they add frame-level process isolation. That might be a tough story for sites deciding whether to use level 2 / SharedArrayBuffers.

Of course, if all browsers do add frame-level process isolation, I'm not sure we gain much from requiring mutual consent in the long run (and why it would be specific to any features gated on level 2). @mystor, are there certain things you're thinking that would provide protection from?

Alternatively, would it be worth considering X-Bikeshed-Allow-Unisolated-Embed as a temporary header, as I think it was described in the Bikeshed proposal doc?

rniwa commented 6 years ago

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.

That's a good point. We probably need to do that. I've updated the proposal.

rniwa commented 6 years ago

One more thing - I don't believe we can re-use frame-src 'self' as it isn't inherited at the moment by subframes. I've thrown together a quick little demo page: https://yielding-bobolink.glitch.me/

Oh, right, we probably need a new CSP directive, or make level-two protection of this header to trigger that behavior. Note that this behavior isn't needed for level-one protection since our proposal is that we would allow websites to load and open cross-origin content as they wish.

csreis commented 6 years ago

@rniwa: While we're on the topic, the new header in your level 1 proposal would still apply to browsers with frame-level process isolation, correct? That is, we would still sever the connection to other windows when navigating to a document with the header.

This wouldn't be necessary for Spectre or for correctness (since it's possible for page A(B) to open B in a new window and have the two B documents in the same process as each other), but I agree that it's still useful from a web security standpoint. The top-level B might reasonably want to make sure no existing window has a reference to it.

arturjanc commented 6 years ago

+1 to also protecting the window from auxiliary browsing contexts that it itself opens as discussed above -- my guess is that the model of "only my own frames or their descendants can get a direct reference to me" is both powerful security-wise and simple to understand/adopt. AIUI it would also be sufficient for Spectre mitigations in browsers without frame-level process isolation.

@csreis / @mystor Would it make sense to try and solve the framing problem you're talking about with CORS? It will likely already be necessary to force pages which are put into "level 2" isolation to load their subresources via CORS (to protect against no-cors-mode loads of non-cooperating resources exempted from CORB, such as images). If we could apply the same requirement to iframes embedded in the isolated document it would prevent the document from iframing non-cooperating sites, and might potentially eliminate the need for a special header (or if we want explicit consent from the embeddee we could still do it via an Access-Control-Allow-* header and reuse CORS infrastructure).

There are two caveats here:

If we did it this way, then I think "Level 2" would boil down to enabling the restrictions of "Level 1", requiring CORS for all fetches initiated by the document, and recursively propagating to the document's iframes (the concept of which already exists in the HTML sandbox and Feature Policy, as mentioned above). I think this would be in line with @rniwa's comment about making the mode more general than as just a Spectre protection (e.g. a similar design was proposed to address some security concerns with the performance.memory API).

rniwa commented 6 years ago

@rniwa: While we're on the topic, the new header in your level 1 proposal would still apply to browsers with frame-level process isolation, correct? That is, we would still sever the connection to other windows when navigating to a document with the header.

Yes! The point of our proposal is to allow Spectre protection in browsers without a frame-level process isolation but making it a useful security feature independent of that.

rniwa commented 6 years ago

@arturjanc : Yeah, something along that line of the thought would probably work, and it would indeed be useful post Spectre.

Somewhat orthogonally, I wonder how important of a use case is to use SharedArrayBuffer and third party ads in a single page. And presumably, we wouldn't let those ads use SharedArrayBuffer because that would allow ads to attack the embedding document? It would be great if someone could clarify that.

By the way, is everyone happy with what we're proposing for level one protection? We like that it's a very simple & easy feature for web developers to understand (e.g. severs window.opener and the window handle in opener's browsing context, and it doesn't seem to preclude any of the level two protections we've discussed so far.

annevk commented 6 years ago

It's not very clear to me how your proposal relates to origins (and whether same-site is being considered).

@arturjanc overloading CORS seems problematic:

  1. CORS isn't designed for navigation and integrating it with navigation (while desired by some to allow new form submission use cases) will be quite involved.
  2. Resources you navigate to typically contain lots of sensitive data. It's unclear we should give read access to all of that.

(I'm a bit surprised nobody gave concrete feedback to what @mystor and I wrote up.)

mystor commented 6 years ago

@csreis

Alternatively, would it be worth considering X-Bikeshed-Allow-Unisolated-Embed as a temporary header, as I think it was described in the Bikeshed proposal doc?

Unfortunately I that header only really makes sense if we are using a temporary header to enable level-2 protections as well. If we are using something like frame-src: 'self' as the hint, the header cannot allow you to bypass that restriction.

@csreis

While we're on the topic, the new header in your level 1 proposal would still apply to browsers with frame-level process isolation, correct? That is, we would still sever the connection to other windows when navigating to a document with the header.

Yes, the plan is to have none of the standardized headers have different effects depending on whether frame-level site isolation is avaliable. The goal is to make generally useful headers. The only major difference right now would be that SAB/WASM threads would only be available in documents which have L2 in non-frame-level-isolated browsers, however that wouldn't be part of the standard.

@arturjanc

If we did it this way, then I think "Level 2" would boil down to enabling the restrictions of "Level 1", requiring CORS for all fetches initiated by the document, and recursively propagating to the document's iframes (the concept of which already exists in the HTML sandbox and Feature Policy, as mentioned above). I think this would be in line with @rniwa's comment about making the mode more general than as just a Spectre protection (e.g. a similar design was proposed to address some security concerns with the performance.memory API).

My main worry here is the same as @annevk's concern, which is that CORS is fairly deeply tied into subresources, and integrating it with navigation could be complicated & error prone. If we decide that this is the approach we want to take we could potentially look into it.

@rniwa

Somewhat orthogonally, I wonder how important of a use case is to use SharedArrayBuffer and third party ads in a single page. And presumably, we wouldn't let those ads use SharedArrayBuffer because that would allow ads to attack the embedding document? It would be great if someone could clarify that.

The primary example I can see here would be if Facebook or similar wanted to set this header in their toplevel embedding page to load a game with access to SharedArrayBuffer from a cross-origin resource. This situation may be something which is too unsafe, and we don't want to be supporting.

@rniwa

By the way, is everyone happy with what we're proposing for level one protection? We like that it's a very simple & easy feature for web developers to understand (e.g. severs window.opener and the window handle in opener's browsing context, and it doesn't seem to preclude any of the level two protections we've discussed so far.

I'm generaly comfortable with it, it appears as though it's a fairly straightforward approach to take.

In effect it does:

The main disadvantage of this proposal is that L1-protected documents have to be very careful when navigating. Specifically, if they open any auxiliary browsing contexts, they cannot navigate to an untrusted cross-origin resource, as the load will be unable to leave the current process.

It may be advantageous to also do a fresh load when navigating away from an L1 isolated document (L2 isolated documents cannot have auxiliary browsing contexts, so it is not an issue for them).

This proposal in general seems like a simpler but more strict version of the proposal made by myself and @annevk above (e.g. it prohibits L2 documents from having any auxiliary browsing contexts), however that may be desirable.

Some more comments:

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.

We should also note that some state needs to be copied between the old and new browsing contexts (specifically session history). We should also note the interactions with BFCache (can the previous document remain suspended? What happens when you go back? Do we un-close browsing contexts when navigating through history?).

In general I think clarity should be added to what happens around session history in the proposal.

@annevk

It's not very clear to me how your proposal relates to origins (and whether same-site is being considered).

I think @rniwa's proposal avoids dealing with the site/origin problem by simply always cutting existing (deny-incoming), or existing and new (deny-all) links. In effect, if you are a root document with this header, you are guaranteed to be in a toplevel browsing context and have no auxiliary browsing contexts.

The problem of same-site vs same would then come down to whatever proposal is used for controlling what documents you're allowed to frame, which appears to still be fairly contended.

rniwa commented 6 years ago

The main disadvantage of this proposal is that L1-protected documents have to be very careful when navigating. Specifically, if they open any auxiliary browsing contexts, they cannot navigate to an untrusted cross-origin resource, as the load will be unable to leave the current process.

It may be advantageous to also do a fresh load when navigating away from an L1 isolated document (L2 isolated documents cannot have auxiliary browsing contexts, so it is not an issue for them).

Wouldn't that break OAuth workflow? One aspect of what we like about our current proposal is that the existing OAuth workflow would continue to work assuming both OAuth provider & OAuth user trust one another.

annevk commented 6 years ago

Note that with our proposal it would also work, as long as the OAuth flow happens in an <iframe> or auxiliary browsing context. If I understand things correctly with your proposal a stray <a href=[notfullytrustedcrossorigin]> could end up being problematic.

rniwa commented 6 years ago

If I understand things correctly with your proposal a stray <a href=[notfullytrustedcrossorigin]> could end up being problematic.

Could you clarify what the problematic behavior is?

annevk commented 6 years ago

notfullytrustedcrossorigin wouldn't end up process-isolated from the current document.

rniwa commented 6 years ago

Oh, yes, a navigation away from an isolated document / browsing context should also sever the opener of the browsing context in https://github.com/whatwg/html/issues/3740#issuecomment-426449368

We like not severing the opener when navigating in an auxiliary browsing context opened by an isolated document in order to keep OAuth working. Namely, if an isolated site a.com in a browsing context A opens non-isolated a.com in an auxiliary browsing context B, and B navigates to OAuth provider b.com, we want b.com in B to be able to communicate with a.com in A. i.e. they should stay in the same process. This seems an okay from the threat model of the website since a.com trusts b.com in this case.

If a.com in A were to open an auxiliary browsing context C which can navigate to any content, e.g. third party news site, then a.com can open the auxiliary browsing context using one of its isolated pages of a.com. Then navigating to any cross-origin page would sever the opener relationship, and would allow UA to do a process swap.

This approach seems to strike a good balance between the isolation we want to provide in order to allow process swaps while retaining the ability for a website to communicate directly with trusted websites.

annevk commented 6 years ago

So the difference between our proposals is that with your proposal even a same-origin/site navigation would end up in a new browsing context (process).

rniwa commented 6 years ago

@annevk : Oops, sorry, we should clarify that the severing happens only across cross-site navigations. I think the key difference is whether top-level browsing context's isolated status inherits to auxiliary browsing context or not.

We think it's better not to do that because that would websites to have a control over what happens (for an easier adaption of the feature), and avoid special-casing top-level browsing context.

annevk commented 6 years ago

No, not even that is different then. (I'm again somewhat dismayed nobody gave feedback on ours, was it that unclear?) That only happens in our proposal for strict (l2) isolation, in that case the auxiliary browsing context would end up being replaced with a new top-level browsing context, otherwise it doesn't really take effect and you wouldn't have to copy the state (it's copied for simplicity since it's needed for strict). We thought that would be more useful than not allowing them to be created at all.

rniwa commented 6 years ago

Hm... we must have misunderstood your proposal then. Sorry, we were focused on describing what we agreed during F2F meeting around the time you made your proposal, and we sort of got carried away.

But it's a good news that it seems like we're mostly on the agreement in terms of what we want for level one protection!

rniwa commented 6 years ago

By the way, are people coming to TPAC? Can we have a breakout session for this topic on Wed?

csreis commented 6 years ago

I won't be at TPAC, but hopefully @arturjanc will? However, I've been spending most of today trying to understand both the Mozilla and Apple proposals in more depth to figure out my questions about them. I'll try to boil it down and share something before TPAC.

csreis commented 6 years ago

Ok, I wrote up my understanding of the Mozilla and Apple proposals, from the perspective of how they relate to Spectre mitigations (Level 1) and enabling SharedArrayBuffers (Level 2), as well as what might be expected of browsers with frame-level isolation (e.g., Chrome). It's a bit long to put into a comment here, so I put in this document: https://docs.google.com/document/d/19eGHUnLaARKpE4PofTNDil5Ix9I-AGYQEB-Rr2V9sfg/edit?usp=sharing

The document is open for comments if you want to reply to anything there, or we can discuss it here if you'd prefer. There's a few things I'm not clear on in the proposals (which I've called out in red in the document), but on the whole I agree that we seem to be getting close.

Hope the document is helpful as a checkpoint for where we are (given the long discussion here), and that I'm not too far off in my understanding!

annevk commented 6 years ago

@csreis thank you so much, great resource! I left a bunch of comments. Glad to see we're mostly on the same page.

csreis commented 6 years ago

Thanks for the comments! You gave some useful clarifications for things I missed. I have a few questions remaining, but it's coming into focus a bit more now. @rniwa, @mystor, and @arturjanc, feel free to take a look as well if you get a chance.

arturjanc commented 6 years ago

Thanks, @csreis! I really like your doc and left some minor comments -- my only request would be to, at some point, translate the proposals into a bit more web-friendly terms so that developers who consider adopting them would understand the consequences for their applications (e.g. the severing of opener relationships) and could provide feedback.

I will be at TPAC and am setting aside time on Wednesday to talk about this (hopefully we can figure out the scheduling during the WebAppSec session).

csreis commented 6 years ago

Thanks. Agreed that we should be getting feedback from developers who might adopt these.

On that note, I was discussing this more with our team today, and it stood out that OAuth providers would not be able to adopt L1 or L2 headers on their OAuth flows with either of these proposals. That actually seems like quite a big problem, since they have a strong incentive for L1, given that users will be entering passwords and getting login tokens in their process. If they did use L1 headers, they wouldn't be able to communicate back to the original site that the login succeeded.

Given that OAuth providers are pretty important to protect, are there any things we can recommend for them? Should we be advocating a different OAuth flow that involves navigating from foo.com to auth.com and back in the same window (with fresh loads each time), passing the token via the URL? (And if they have to do that anyway, does that reduce the need for us to keep cross-origin popups in an L1 document's process?)

In general, this reminds me of the issues covered in my earlier writeup from May, though we didn't seem to like the "credential-less subframe" possibility mentioned there.

annevk commented 6 years ago

F2F notes thanks to @arturjanc. Unfortunately @csreis's feedback did not get discussed. A summary of the headers we did discuss:

Cross-Origin-Opener-Policy = sameness [ RWS outgoing ]
Upgrade-No-Cors = sameness
Cross-Origin-Frame-Policy = sameness

sameness = %s"same-origin" / %s"same-site" ; case-sensitive
outgoing = %s"unsafe-allow-outgoing" ; case-sensitive

It seems if we decide that OAuth needs to be L1-protected we can remove the [ RWS outgoing ] bit. (So every window opened is as if noopener was specified.)

A special thing with Upgrade-No-CORS not discussed at the meeting is that it would have to inherit into dedicated workers as well (it already inherits into sameness nested browsing contexts). Other workers are never in the same agent cluster so they need to have it set on them specifically (also, inheritance would be racy). (This header is also independently useful for things like the memory API. Perhaps we can also bind navigator.storage.estimate() to it...)

Cross-Opener-Policy is probably the hardest to define given the generally unsound infrastructure, but probably still doable. I don't know if anyone was planning on working on this, but I'd be happy to help out.

Feedback on this from @whatwg/security is welcome.

mystor commented 6 years ago

In terms of OAuth providers being L1 or L2 protected, I think they (technically) could do it, but it would be messy... OAuth providers need the opener relationship to communicate info back to the requestor, and if the provider is L1 isolated it won't have any opener relationship to the requestor.

Off of the top of my head, I think the OAuth provider could work around this by:

  1. Embedder embeds provider-origin iframe (e.g. the "button")
    • NOTE: This frame would have to have no private information, as it lives in-process with the embedder.
    • NOTE: L2-protected sites likely couldn't use OAuth as they would be unable to frame this element.
  2. iframe generates a unique "public" ID
  3. iframe creates BroadcastChannel & registers listener waiting for msg with "public" ID
  4. iframe opens OAuth UI (which would sever its opener) with "public" ID in query params
  5. OAuth UI sends A BroadcastChannel message with reply & "public" ID.
    • NOTE: To avoid leaking info to other embedded frames, the reply would probably have to be minimal, and the iframe would have to then fetch the remaining information using some other mechanism (e.g. HTTP requests). For example, it would be a bad idea to directly include API keys in the message, as multiple frames will get the reply.
    • (See below for a workaround)
  6. The frame would then propagate the necessary info to the embedder

As to what would be in that reply mesasge, it might have to look something like:

  1. Before opening UI, iframe creates "private" unique ID
  2. iframe passes this "private" ID and the "public" ID to the OAuth UI with query params.
  3. OAuth UI, after logging in, sets up a endpoint which will accept a single request with the "private" key to get the real API key.
  4. OAuth UI sends a BroadcastChannel message with only the "public" ID to tell iframe response is ready.
  5. iframe fetches the known endpoint to acquire API key
  6. iframe passes key out to embedder.

Alternatively, the communication could be entirely handled by the provider's server, using WebSockets or similar.

Unfortunately, I think this flow is super error-prone and easy to get wrong, so I imagine that it won't get much adoption.

mystor commented 6 years ago

If we want to preserve the opener relationship from a potentially-malicious page to the OAuth UI page, we would have to change strategy. The recent discussions have all been centered around performing isolation in a simple yet permissive manner: namely by removing existing opener relationships which would force the page to be loaded in-process. This is pretty fundamentally at-odds with the OAuth UI use-case.

If I remember correctly, the reason we chose to go this way is that it:

  1. Doesn't change the behaviour of 'same origin-domain' for platform objects, and
  2. Has utility for websites wanting to remove these relationships to protect themselves beyond Spectre.

The other options which we discussed earlier centred around changing the WindowProxy behaviour. These proposals ended up failing because, in order to achieve isolation, we need to change the behaviour of WindowProxy objects for same origin-domain documents. Otherwise, an attacker document could frame a same origin-domain document which would need sync access to our isolated document.

If we were comfortable changing the behaviour of WindowProxy objects for same origin-domain documents, we could do something like:

  1. The header sets an isolated flag on the Environment Settings Object for its associated realm. Nested browsing contexts inherit this isolated flag from their parent, and set it on all created ESOs.
  2. IsPlatformObjectSameOrigin(O) is modified to add an additional clause (also potentially renamed):
    • Return false if the either the current settings object or O's relevant settings object's isolated is set.
    • Return true if the current settings object's origin is same origin-domain with O's relevant settings object's origin, and false otherwise.

In effect, this would make WindowProxy objects of isolated documents act as though they are dissimilar-origin for the purposes of script access, meaning that we can put tabs with this header in new processes, as they, and their subframes, are dissimilar-origin from all other documents. Unfortunately this would prevent same-origin access to subframes. This could be worked around by also comparing the toplevel browsing contexts of the current and relevant globals.

csreis commented 6 years ago

@mystor: Thanks. I agree that there are downsides to treating same-origin frames as if they were cross-origin, since that can get confusing. It may be possible to go down that path if needed, but you also point out some ways OAuth providers might be able communicate across units of related browsing contexts (though they may have their own downsides). And it does seem appealing to have a header that is useful independent of Spectre protections, so I agree there's some appeal to the approach of severing window references.

Maybe we should seek feedback from potential adopters of these headers (including OAuth providers) to confirm we're building something useful for them?

@annevk: Thanks for the summary. Can you elaborate on what Upgrade-No-Cors and Cross-Origin-Frame-Policy do? The writeup is a little short on details.

I'm also curious if the L2 part needs to continue blocking cross-origin iframes if browsers gain support for frame-level process isolation. I'm not sure what the long-term benefits are in that case (the way that severing window references is useful outside Spectre). Loading those iframes in a separate process seems safe even if the parent document has access to SharedArrayBuffers and other precise timers.

annevk commented 6 years ago

@csreis in browsers without cross-process frames you need something like Cross-Origin-Frame-Policy to restrict what descendant frames can navigate to. Navigation across the "sameness" boundary would result in a network error. This header has somewhat less utility with cross-process frames.

Upgrade-No-CORS inherits into any nested or popup frame, or dedicated worker. It sets a flag checked by Fetch that will change all "no-cors" fetches to "cors" fetches. This way whoever uses the header cannot perform attacks on "no-cors" resources (similar to CORB, but for all resources except navigations).

The combination of these headers (and all having equal "sameness" values) would enable high resolution timers.