Closed bvandersloot-mozilla closed 7 months ago
I have an idea to remove the additional round-trip and to reuse some infrastructure.
If I understand correctly, this doesn't remove the additional round-trip; it converts it into a CORS preflight instead of a traditional GET. Right? And this is all gated on whether the top-level document used the new HTML attribute, so it requires work/coordination from both the top-level and the embed in order to use this properly.
IIUC, we'd still need some kind of new response header to allow the server to say "please load this content with storage-access activated", in order to skip the additional round-trip for iframes that don't themselves require auth cookies, but which load subresources that do require auth cookies. (This is what the load
token does in the current proposal.) And if we still have that response header, then the server needs to know when it's reasonable to use load
, so we still need the inactive
request header.
So IMO, the design you suggest:
load
response token, and therefore spreads this feature into both old CORS headers and a new header, instead of handling both in the same response header.load
response token, and therefore regresses performance by forcing the iframe to execute JS and then refresh (same status quo as today).storage-access
permissions policy is "*"
).So while it sounds nice to reuse CORS infrastructure, I don't think it actually gains us anything, and it seems to make things worse IMO. WDYT?
Sorry- that should say "I have an idea to remove the additional round-trip mechanism...". I was thinking of complexity of the platform as a constraint here.
If I understand correctly, this doesn't remove the additional round-trip; it converts it into a CORS preflight instead of a traditional GET. Right?
Yes, however, now that I look at it again, I am not sure if we need to make these requests non-simple. Simply making these requests protected by CORS should be enough to have an opt-in signal. This would remove the round-trip.
Either: Keeps the load response token, and therefore spreads this feature into both old CORS headers and a new header, instead of handling both in the same response header. Abandons the load response token, and therefore regresses performance by forcing the iframe to execute JS and then refresh (same status quo as today).
Or iframes with "use-storage-access" just set the has storage access
bit of their document if they have storage access.
Makes usage harder, since now the top-level page must be modified.
This seems like reasonable usage to me, and probably easier than adding HTTP header-handling for most developers.
This doesn't scale for non-iframe resources that are embedded in many different top-level sites.
This is no worse than cross-origin="use-credentials"
which should be how those resources are doing this now, no?
It's inconsistent with the JavaScript ergonomics (since the default allowlist for storage-access permissions policy is "*").
I'll give you this. However, I don't think this is an issue. Forcing better hygiene to use a more performant option is good.
Yes, however, now that I look at it again, I am not sure if we need to make these requests non-simple. Simply making these requests protected by CORS should be enough to have an opt-in signal. This would remove the round-trip.
The problem with this is that this opt-in signal is coming from the embedding site (the one that embeds the subresource), which is (by assumption) different from the embedded site (the one that supplies the subresource). An HTML attribute doesn't let us conclude that the embedded site is opting in.
Similarly, we can't use use-storage-access
to just set the has storage access
bit in the iframe's document, since that would let a malicious embedder mount a CSRF attack on a victim iframe, without giving the iframe any choice in the matter.
This seems like reasonable usage to me, and probably easier than adding HTTP header-handling for most developers.
Maybe your opinion has changed based on what I said above (re: security properties), but if we did require a use-storage-access
HTML attribute, what does that get us?
Considering that we still need a response header to indicate opt-in from the 3p server (IIUC), I think that additionally requiring the HTML attribute would just allow a top-level document to forbid credentialed cross-site subresource requests (by just omitting the attribute everywhere). IMO, the simpler way to achieve that is to apply a storage-access permissions policy of self
to the whole top-level document.
I guess the attribute allows the embedder to be selective about which subresources can use credentials and which can't? But I'm struggling to see the utility of that, considering the user has already given storage-access permission so they do not want to enforce a privacy boundary between this pair of sites.
The problem with this is that this opt-in signal is coming from the embedding site (the one that embeds the subresource), which is (by assumption) different from the embedded site (the one that supplies the subresource). An HTML attribute doesn't let us conclude that the embedded site is opting in.
The HTML attribute isn't the opt-in signal I was talking about here. The CORS Access-Control-Allow-Credentials
is the third-party's opt in.
The HTML attribute is a little orthogonal, and is just a way to determine which resources should be unpartitioned. I suppose there is a design principle question here: should the client or the server determine which resources are unpartitioned. I favor the client. That is in line with the Javascript API, leaves room to remove the round trip for unpartitioned resources, and it just makes more sense to me that the document makes decisions about when to try to use unpartitioned cookies.
Ah, I misunderstood. So if I'm understanding correctly, you're suggesting that the browser would always send 3p cookies if the storage-access
permission has been granted and the request is CORS-enabled?
(And the browser would probably also send 3p cookies even without CORS if the storage-access
permission has been granted and "activated" by a call to requestStorageAccess()
, for backward-compat reasons.)
That sounds reasonable to me in terms of privacy/security, and it does avoid the extra round-trip. That's pretty nice.
So the remaining piece is the load
token's semantics; I think it would still be good to support Activate-Storage-Access: load
(or some other spelling) so that iframes can load their own subresources via credentialed requests immediately.
Ah, I misunderstood. So if I'm understanding correctly, you're suggesting that the browser would always send 3p cookies if the
storage-access
permission has been granted and the request is CORS-enabled?
Not always, but where an HTML attribute asks for them specifically. And that would force CORS mode.
(And the browser would probably also send 3p cookies even without CORS if the
storage-access
permission has been granted and "activated" by a call torequestStorageAccess()
, for backward-compat reasons.)
:+1:
That sounds reasonable to me in terms of privacy/security, and it does avoid the extra round-trip. That's pretty nice.
Thank you!
So the remaining piece is the
load
token's semantics; I think it would still be good to supportActivate-Storage-Access: load
(or some other spelling) so that iframes can load their own subresources via credentialed requests immediately.
If the embedee marking the iframe as "use-storage-access" isn't good enough (presumably because only requiring 3rd-party changes is easier in the popular embed use-case?), including a new HTTP Response header for frame content that has the semantics of "Activate-Storage-Access: load
" isn't bad. A spelling of Accept-Unpartitioned: ?1
suddenly comes to mind: the server negotiates the headers of future requests.
Hm, on second thought, one intentional property in my original design was that this didn't require any work on the part of the top-level site; the 3p could manage things entirely by itself with the request header and response header. That makes the headers a little different from CORS, since CORS (and the use-storage-access
HTML attribute) requires an opt-in from the top-level, as well as a response header from the 3p.
I could certainly see a world where both designs coexist, so that the top-level site can do some work in order to avoid the extra latency from the additional round trip. But I want to avoid introducing an unnecessary requirement that the top-level site must do that work.
I suppose there is a design principle question here: should the client or the server determine which resources are unpartitioned. I favor the client. That is in line with the Javascript API, leaves room to remove the round trip for unpartitioned resources, and it just makes more sense to me that the document makes decisions about when to try to use unpartitioned cookies.
The principle I've been using is embeddee vs embedder, rather than server vs client. I.e., the embeddee must be the (only) one that determines which of its resources are fetched using unpartitioned cookies, for security reasons. The embeddee already has the ability to do that on the client (via document.requestStorageAccess()
, if it has an iframe already), but doesn't have the ability to do it on the server yet (for the cases where it doesn't have an iframe).
And conversely, it's important to not require the embedder's participation, because we're already seeing cases where a cross-site resource is embedded in lots of different top level sites (think any SaaS provider). Updating all of those top level sites is not necessarily feasible, nor is it really necessary.
The principle I've been using is embeddee vs embedder, rather than server vs client.
I get that, and I think that is an important principal. How about an HTML vs Fetch distinction? Because to me, fundamentally what you want to do is alter an environment, and in order to do that it would be best to make changes to the HTML rather than the Fetch. Like throwing a use-storage-access
on the iframe's <html>
element to attempt a permission activation. That would preserve the Activate-Storage-Access: load
case, and we could even have an option to force a refresh where the permission is activated.
I see Access-Control-Allow-Credentials
and cross-origin="use-credentials"
as solving almost this exact problem in the past and think a major change in structure of the solution requires a compelling reason. Zero-change for the embedder is a good reason, but if we can design for it, it would be nice to keep this the same shape. I'd also like to prevent having two ways to do it if we can avoid it too.
How about an HTML vs Fetch distinction? Because to me, fundamentally what you want to do is alter an environment
I assume you're talking about the embeddee's environment, i.e. the iframe case with Activate-Storage-Access: load
?
If so - we already have most of the environment
changes we need, via the Storage Access API spec. I think all we'd need to add is a step to set request
's reserved client
's has storage access
to true when the Activate-Storage-Access: load
header is present. (I think cross-site redirects complicate this slightly, but it should be pretty close to this in principle.)
If you mean changing the embedder's environment (i.e. the top-level document), that seems impossible to do without also requiring changes in the embedder's HTML.
I see Access-Control-Allow-Credentials and cross-origin="use-credentials" as solving almost this exact problem in the past and think a major change in structure of the solution requires a compelling reason.
Those are also about handling cross-origin requests, but they attack the problem from a slightly different angle. They allow the embedder to ask for credentials to be included in the cross-origin request.
I'm trying to give the embeddee a way to ask for credentials to be included in a request to its origin. The embeddee can't supply the crossorigin
attribute, so that doesn't give the embeddee a way to ask for its credentials.
IIUC, CORS fundamentally requires opt-in from both parties (the sender/embedder via cross-origin=...
; the recipient/embeddee via Access-Control-Allow-Credentials
). So I like the idea of allowing the embedder to include credentials (if the storage-access
permission is granted) via cross-origin=...
(since that still requires the embeddee's approval eventually, via CORS). But I don't think it's viable to require the cross-origin=...
attribute, since that means the embeddee can't unilaterally ask for its credentials.
I'd also like to prevent having two ways to do it if we can avoid it too.
I think we ought to be careful about this principle. The embeddee currently doesn't have any ways of asking for their credentials (unless it's capable of executing JS); so I think we ought to count this header as a distinct capability, rather than an additional way of doing something that's already possible.
I also think it's ok to have more than one way to do something if one way presents better tradeoffs than the other in some cicumstances. E.g., it's fine to provide the load
token instead of executing document.requestStorageAccess()
, since that allows the client to avoid unnecessary subresource loads and JS execution (presuming that permission has already been granted).
How about an HTML vs Fetch distinction? Because to me, fundamentally what you want to do is alter an environment
I assume you're talking about the embeddee's environment, i.e. the iframe case with
Activate-Storage-Access: load
?
Yep! I'm pointing out that the thing whose state we are changing is a Document, so putting the change in HTML is a closer match to the current semantics where the document calls requestStorageAccess. I'm not saying that this would be hard to spec up, just trying to think where we should add this functionality.
I see Access-Control-Allow-Credentials and cross-origin="use-credentials" as solving almost this exact problem in the past and think a major change in structure of the solution requires a compelling reason.
Those are also about handling cross-origin requests, but they attack the problem from a slightly different angle. They allow the embedder to ask for credentials to be included in the cross-origin request.
I'm trying to give the embeddee a way to ask for credentials to be included in a request to its origin. The embeddee can't supply the
crossorigin
attribute, so that doesn't give the embeddee a way to ask for its credentials.
Fair! But getting a little creative here: a <meta>
tag in the embeddee could take the role of the embeddee's opt in. Like an iframe with <meta name="storage-access" value="blah">
could be the same as Activate-Storage-Access
response header in your proposal, with a value for load
and another for retry
.
We should try to support use cases where the web dev doesn't have control of the server where possible, and I think we can. That and not reinventing a CORS-like mechanism are why I'm trying to help here.
I think we ought to be careful about this principle. The embeddee currently doesn't have any ways of asking for their credentials (unless it's capable of executing JS); so I think we ought to count this header as a distinct capability, rather than an additional way of doing something that's already possible.
Agreed that JS and non-JS ways of doing a thing are distinct capabilities.
I also think it's ok to have more than one way to do something if one way presents better tradeoffs than the other in some cicumstances. E.g., it's fine to provide the
load
token instead of executingdocument.requestStorageAccess()
, since that allows the client to avoid unnecessary subresource loads and JS execution (presuming that permission has already been granted).
👍, agreed
Yep! I'm pointing out that the thing whose state we are changing is a Document, so putting the change in HTML is a closer match to the current semantics where the document calls requestStorageAccess.
Ah I see, I misinterpreted!
FWIW, CSP and Permissions Policy are two examples of headers that integrate with the HTML spec, so there's precedent for modifying Document
state based on a response header - particularly one related to security.
Fair! But getting a little creative here: a tag in the embeddee could take the role of the embeddee's opt in. [...]
That's an interesting idea! That feels similar to how Content Security Policy supports delivery via a \ tag. I'm not opposed to supporting something like that in the future, to help use cases where the web dev doesn't control the server.
I do also want to support use cases where modifying the content isn't an option, though, (since if the content can be modified, then they have options today; they could just call requestStorageAccess()
). If the content can't be modified, then there aren't really options available today, which is part of what I'm trying to build for.
not reinventing a CORS-like mechanism are why I'm trying to help here.
I've been thinking about this more, and I'm becoming convinced that we do have to reinvent a CORS-like thing:
I think this mechanism and CORS are sort of complementary to each other, and have a lot in common. In particular:
cors
and there's an existing storage-access
permission grant, the browser could attach unpartitioned cookies, IMO.
retry
part) will have to handle non-idempotent request methods with a preflight of some kind, same as CORS does.
WDYT?
Catching up with this (thanks for the great discussion!), and at the risk of being biased 😆, I think I'm supportive of Chris' direction and ideas here.
Two principles I'd really like to uphold with this proposal:
Since these are both challenges we've heard quite frequently from developers.
I also support the idea of reducing reliance on servers (and server libraries) to support the right headers to use SAA, this could be especially helpful in environments where these headers can't be easily modified (CDNs, I think?). However, it feels orthogonal to the problems being attacked by this proposal, and SAA is already usable with only client-side code.
Overall I'm skeptical about the overloading of CORS to solve storage access opt-ins as a long term web platform solution. As Chris mentioned, it only gets us halfway there in terms of security benefits. We're using it as the top-level opt-in mechanism for rSAFor, which is criticized as insufficient in https://github.com/privacycg/requestStorageAccessFor/issues/30. Besides that, I've also come to realize that overloading these existing concepts for new exciting purposes often causes additional effort and pain for everyone involved, including the folks who maintain CORS and now have to watch out for side effects they didn't design for. cc @arturjanc
There's probably a lot to write about this but I think I'd prefer to not further enshrine it on the web platform until we have more clarity on these questions. We can explore it, though!
These headers (particularly the retry part) will have to handle non-idempotent request methods with a preflight of some kind, same as CORS does.
Unfortunately yes, I think you're right.
I am coming around on this being a new header set- the issue specifically being the fallback case. In CORS it is an error and here we would want it to just send without unpartitioned credentials.
Okay, it makes sense that we could have these headers co-defined in meta tags. We can punt on the network-only-ness, and just slap on meta http-equiv definitions at the end.
These headers (particularly the retry part) will have to handle non-idempotent request methods with a preflight of some kind, same as CORS does.
Oof. Looking forward to your writeup on the problem. My gut tells me it may be easiest to forbid retry on non-idempotent request methods, especially due to the change in credentials between the two requests.
Re: HTML vs Fetch, I also realized that in order to require a change in the HTML (e.g. a <meta>
tag or something), there has to be some embedded HTML. I think this unfortunately disqualifies things like the IIIF use case (which embeds images, not iframes). To allow some non-HTML option, I don't think we have a choice but to allow a Fetch-only usage/integration.
Continuing from #2.
I have an idea to remove the additional round-trip and to reuse some infrastructure.
This entails two conceptual changes. 1. Move the activation of storage access to the client, rather than the server. 2. Use CORS preflights to maintain server control over whether or not unpartitioned cookies are ever sent.
To accomplish 1, we don't have a
Activate-Storage-Access:
header and use a new HTML attributeuse-storage-access
. This attribute would only be valid on frames or wherecross-origin="use-credentials"
is present and says to use unpartitioned cookies if thestorage-access
permission isGranted
. This would do some stuff in Fetch that makes it use the right cookie jar (cue Johann's cookie layering).To accomplish 2, we need to make the requests with unpartitioned cookies not "simple". That forces a preflight which the server can make security decisions on.
We would still need an informational header to inform the server of whether or not the request was unpartitioned, but may not need the detail about inactivity. Maybe reviving something like Dylan's proposals in https://github.com/w3c/webappsec-fetch-metadata/issues/80 would make sense.
Overall I think this is a cleaner design because it treats unpartitioned credentialed requests as an "unsafe thing" in the construction for "unsafe things" we already have.
Continuing to think about this - I was wrong about needing to design a preflight mechanism, similar to CORS. I wrote up an argument why preflights are unnecessary in https://github.com/cfredric/storage-access-headers/commit/adbfd5a12b8381c41daa73f2215e15bba231ee72.
Since CORS preflights are not for protecting the semantics of non-idempotent requests (after all, POST requests aren't preflighted), we don't need to handle non-idempotent requests specially here either; a server can respond with a 4XX and the retry
header if desired.
There's been a bunch of discussion on this issue (sorry I missed it, I was OOO and never managed to get to this in my post-Christmas backlog) and I think the proposal has changed in the meantime, so just adding a few random thoughts:
cors
mode to an origin that obtained storage-access
permission, as long as the request is made with { credentials: 'include'}
. CORS has the necessary opt-ins to make this mostly safe (e.g. the server needs to set the Access-Control-Allow-Credentials: true
and Access-Control-Allow-Origin: https://embedding-origin.example
headers.
Activate-Storage-Access
header.no-cors
subresource requests. The good thing is that AFAIK there is no way to make requests with non-idempotent methods (no-cors
requests can only be GETs; the only exception are <form>
submissions via POST). So I don't think there's anything special that needs to happen for retry
here.no-cors
resources with credentials without requiring upgrading them to CORS. So the reasons @johannhof mentioned above mostly make sense to me. 1. Re: CORS: It would be _almost_ okay from a security perspective to attach credentials to the embedder's requests in `cors` mode to an origin that obtained `storage-access` permission, as long as the request is made with `{ credentials: 'include'}`. CORS has the necessary opt-ins to make this _mostly_ safe (e.g. the server needs to set the `Access-Control-Allow-Credentials: true` and `Access-Control-Allow-Origin: https://embedding-origin.example` headers. * The one big caveat here is that CORS allows making non-preflighted POST requests and learn response timings for cross-origin requests (@johannhof the same issues we discussed in the context of CORS for ABA embeds). * Because of this, I think it would be safer if we required SAA headers as an opt-in here, rather than piggyback on CORS. This way, the embedder won't be able to make unconstrained requests to the embeddee origin, but only to endpoints that set the `Activate-Storage-Access` header.
This could be addressed with an addition to the fetch's RequestInit
, or even adding redefining the RequestCredentials
enum to include a new value "include-unpartitioned"
. Then forcing requests with that specification to be pre-flighted.
3. Should we even integrate with CORS/Fetch in the first place?
[...] because we want to support loading no-cors resources with credentials without requiring upgrading them to CORS.
I missed this argument, but I disagree with this approach. Allowing the use of unpartitioned credentials where you wouldn't otherwise allow partitioned credentials seems unwise. And saving developers the trouble of upgrading their endpoints to CORS is not helpful if we are forcing them to do an equivalent opt-in anyway.
[...] But I think the overlap is limited enough that a separate header might still be simpler.
I think the overlap is significant enough that it would be simpler to integrate 😄
@bvandersloot-mozilla how would this work with navigational requests and/or requests that the 1P is unable to mark as crossorigin? It feels like the 3P should be able to enforce this mode on its own.
[...] because we want to support loading no-cors resources with credentials without requiring upgrading them to CORS.
I missed this argument, but I disagree with this approach. Allowing the use of unpartitioned credentials where you wouldn't otherwise allow partitioned credentials seems unwise. And saving developers the trouble of upgrading their endpoints to CORS is not helpful if we are forcing them to do an equivalent opt-in anyway.
Reading back- I think I missed the point on this one. If I have it right now, these would be no-cors
requests that were already with credential mode "include"
. In which case I take this back!
@bvandersloot-mozilla how would this work with navigational requests and/or requests that the 1P is unable to mark as crossorigin? It feels like the 3P should be able to enforce this mode on its own.
I forgot about the no-change-on-the-1p constraint. Combined with my re-read above and the lack of a need for preflights, maybe it would be easier to have independent. I could see either being easier still.
I can't shake the feeling that this is about giving the server the power to permit resource sharing based upon the parameters of the request, which is dead on with CORS's purpose in my mental model.
Reading back- I think I missed the point on this one. If I have it right now, these would be
no-cors
requests that were already with credential mode "include". In which case I take this back!
Yes, in my mental model these are loads via <img>
, etc. which by default include credentials (except if third-party cookie restrictions kick in). It seems useful to allow the use of SAA headers for such resources without requiring the 1P to switch them to be loaded via CORS (by adding the crossorigin
attribute).
I can't shake the feeling that this is about giving the server the power to permit resource sharing based upon the parameters of the request, which is dead on with CORS's purpose in my mental model.
I see your point, but here we're just talking about granting the capability to send a credentialed request to the resource without exposing the contents of that resource to the requester.
If you opt into CORS you grant the requester access to the bytes of the resource (and the resource load can have credentials if the requester sets credentials=include
in Fetch and the server opts in by setting the right ACAC and ACAO headers).
With SAA headers you say "I'm allowing this request to have credentials, but I'm not sharing the contents of the resource with you". E.g. imagine a 3P widget that directly renders the user's profile picture as an image on the embedding page (where the 3P knows the user because it's an IdP or something like that); for loads like that we'd want the image to be loaded with credentials, but without CORS to not expose it to the embedder.
This obviously gets murky because no-cors
loads are generally leaky (e.g. images expose their dimensions to the embedder and can be read from the embedder's renderer memory via Spectre, etc). But in terms of the web's security model there's still a difference between allowing embedding and directly exposing the resource to the embedder. CORS is meant for the latter, which is different from the capability to send authenticated requests which SAA headers are trying to allow - IMHO this would be an argument for decoupling this from CORS.
But in terms of the web's security model there's still a difference between allowing embedding and directly exposing the resource to the embedder. CORS is meant for the latter, which is different from the capability to send authenticated requests which SAA headers are trying to allow - IMHO this would be an argument for decoupling this from CORS.
Reading this out, I think this distinction is significant and makes sense to me.
I have another point, but it strays from integration, so I'm filing #8.
Thanks all for the discussion. I'll do my best to summarize:
With all that in mind, I think we've reached consensus that CORS ought to be neither necessary nor sufficient for including unpartitioned cookies, and therefore it's best for security (and usability) if the Storage Access Headers are a separate thing, independent from CORS.
I'll tentatively close this issue, but feel free to correct me if I've misunderstood anything.
Continuing from #2.
I have an idea to remove the additional round-trip and to reuse some infrastructure.
This entails two conceptual changes. 1. Move the activation of storage access to the client, rather than the server. 2. Use CORS preflights to maintain server control over whether or not unpartitioned cookies are ever sent.
To accomplish 1, we don't have a
Activate-Storage-Access:
header and use a new HTML attributeuse-storage-access
. This attribute would only be valid on frames or wherecross-origin="use-credentials"
is present and says to use unpartitioned cookies if thestorage-access
permission isGranted
. This would do some stuff in Fetch that makes it use the right cookie jar (cue Johann's cookie layering).To accomplish 2, we need to make the requests with unpartitioned cookies not "simple". That forces a preflight which the server can make security decisions on.
We would still need an informational header to inform the server of whether or not the request was unpartitioned, but may not need the detail about inactivity. Maybe reviving something like Dylan's proposals in https://github.com/w3c/webappsec-fetch-metadata/issues/80 would make sense.
Overall I think this is a cleaner design because it treats unpartitioned credentialed requests as an "unsafe thing" in the construction for "unsafe things" we already have.