Closed rniwa closed 4 years ago
/cc @whatwg/security. Very interesting...
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.
Note that this feature has been implemented in Safari 12: https://developer.apple.com/safari/whats-new/
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.
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.
+-----------+ +-----------+
| 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
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
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.
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:
A
and B
are in Related Browsing Contexts, andA
and B
are Same-Site, andA
and B
are either both isolated or both non-isolated, andA
and B
share a Toplevel Browsing Context, orA
and B
are non-isolated and have non-isolated globals in both of their Toplevel Browsing Contexts.These are a few notable implications:
postMessage
.As navigating by named lookups can also create cross-global references (due to the opener property), we also have to restrict the allowed to navigate check, adding 2 new checks:
A
's or B
's global is isolated, then:
A
's and B
's global have different Window Agents, return falseA
and B
have different Toplevel Browsing Contexts:
A
's or B
's Toplevel Browsing Context's global is isolated, return falseThis should bring the checks in line with the Window Agent selection process.
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:
A
and B
are Same Origin-Domain, andA
and B
are in the same Window AgentThis would change Window Agent selection to define access control, rather than being a result of it.
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.
@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:
Cross-Origin-Resource-Policy
header to same-site
or same-origin
(See https://github.com/whatwg/fetch/issues/687)Cross-Origin-Window-Policy
header to Deny
or Allow-PostMessage
.X-Frame-Options
header to DENY
or SAMEORIGIN
or CSP frame-ancestors
.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.
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.
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.
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.
@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.
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.
@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.
@tomrittervg Thanks for pointing that out. Fixed.
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.
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).
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):
If neither of oldDoc
and doc
are isolated, skip the following steps
NOTE: This is the current case of
unisolated => unisolated
loads.
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.
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 contextisolated(foo.com) => unisolated
- protect the isolated document from unisolated document being loadedunisolated => isolated(foo.com)
- protect the isolated document from previously loaded documents
newContext
in a new window agentcontext
into newContext
context
into newContext
doc
into newContext
context
with newContext
in the browser uicontext
Sites should be careful with which documents they frame, potentially using CSP for added control
Sites must use
X-Frame-Options
to protect themselves
Sites must create auxiliary browsing contexts with
noopener
to protect themselves
This will be mitigated by CORB and other strategies
postMessage
edges are not supported.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.
- If
doc
has an isolated original opener documentopener
, and the to-be-loaded document is isolated,doc
is the same asopener
- Otherwise,
doc
is unisolated
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:
Allow-PostMessage
is set, all WindowProxy references to the original browsing context, context
, have the internal [forwardPostMessage]
property set to newContext
Window::postMessage
method where this
is a WindowProxy to a closed window, check [forwardPostMessage]
. If it is set, perform the following steps:
source
be the calling contexttarget
be the context stored in [forwardPostMessage]
message
using the structured clone algorithm.target
's origin does not match targetOrigin
, abort these stepsevent
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 directionevent
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.
Non-Protections
- 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.
- 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.
- 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.
- Documents with this header may be loaded by cors-exempt subresource loads This will be mitigated by CORB and other strategies
Right.
Side-Effects
- 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.
- Existing postMessage edges are not supported.
Right.
Potential Variations.
- 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?
I had a chat about an alternative model with @mystor.
Ingredients:
When creating an auxiliary/nested browsing context:
When navigating:
If one of the following is true
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.)
Tradeoffs:
Goals:
UI:
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.
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.
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.
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.
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.
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.
@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
.
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.
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/
@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?
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.
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.
@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.
+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:
no-cors
mode into the attacker's address space. 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: 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.
@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.
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:
(I'm a bit surprised nobody gave concrete feedback to what @mystor and I wrote up.)
@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 useSharedArrayBuffer
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:
noopener
for links.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.
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.
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.
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?
notfullytrustedcrossorigin
wouldn't end up process-isolated from the current document.
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.
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).
@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.
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.
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!
By the way, are people coming to TPAC? Can we have a breakout session for this topic on Wed?
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.
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!
@csreis thank you so much, great resource! I left a bunch of comments. Glad to see we're mostly on the same page.
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.
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).
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.
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.
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:
As to what would be in that reply mesasge, it might have to look something like:
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.
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:
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:
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.isolated
is set.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.
@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.
@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.
Proposal
Add HTTP header called
Cross-Origin-Window-Policy
, which takes a value ofDeny
,Allow
, andAllow-PostMessage
.When the HTTP response of a document has a
Cross-Origin-Window-Policy
header, and the value case-insensitively matchesDeny
ignoring cases, the document is said to be fully isolated. If the value case-insensitively matchesAllow-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 exceptpostMessage
on the window proxy of a cross-origin document results in aSecurityError
. The restriction between two documents are symmetrical and the most stricter of the two prevails.Furthermore, a new step is inserted into the concept of allowed to navigate before step 1: If B and/or A is isolated and A and B are not of the same origin, return false.
Examples
Let document A and document B be distinct documents its own browsing contexts. If A and B are of the same origin, the header has no effect. If A and B are cross-origin, then:
If document B is fully isolated and document A is not isolated. Any attempt to access a property on document B's window from document A results in a
SecurityError. Any attempt to access a property on document A's window from document B also results in a
SecurityError`.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 aSecurityError
. Any attempt to access a property exceptpostMessage
on document A's window from document B results in aSecurityError
.If document B is isolated with messaging and document A is fully isolated. Any attempt to access a property on document B's window from document A results in a
SecurityError
. Any attempt to access a property on document A's window from document B results in aSecurityError
.If document B is isolated with messaging and document A is isolated with messaging. Any attempt to access a property except
postMessage
on document B's window from document A results in aSecurityError
. Any attempt to access a property exceptpostMessage
on document A's window from document B results in aSecurityError
.Spectre Protection Plan
For the purpose of protecting a website
a.com
from Spectre in browsers which support process swap for top-level navigations without frame-level process isolation,a.com
can set this header on all of its documents (not setting on some would result in leaks; more on this later).If this header is set on
a.com
, we can swap process on cross-origin navigation from or toa.com
's documents because this header guarantees thata.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
. Thenb.com
doesn't have access toa.com
, andb.com
doesn't have access toa.com
so we can put them into two different processes. Obviously,a.com
's iframes don't have access tob.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 inb.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.