Skip to content

Commit

Permalink
Rewrite retry token to require an allowed-origin parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
cfredric committed Jul 18, 2024
1 parent 06bd0f7 commit f0f43c3
Showing 1 changed file with 16 additions and 10 deletions.
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Instead, we can imagine a different flow, where the user agent recognizes that t
1. The user agent requests the calendar widget's content.
* This fetch is still uncredentialed, as before.
* Since the request is for calendar.com in the context of example.com, and the user has already granted the `storage-access` permission in <calendar.com, example.com> contexts, the fetch includes a `Sec-Fetch-Storage-Access: inactive` header, to indicate that unpartitioned cookie access is available but not in use.
1. The server responds with a `Activate-Storage-Access: retry` header, to indicate that the resource fetch requires the use of unpartitioned cookies via the `storage-access` permission.
1. The server responds with a `Activate-Storage-Access: retry; allowed-origin=<origin>` header, to indicate that the resource fetch requires the use of unpartitioned cookies via the `storage-access` permission.
1. The user agent retries the request, this time including unpartitioned cookies (activating the `storage-access` permission for this fetch).
1. The server responds with the iframe content. The response includes a `Activate-Storage-Access: load` header, to indicate that the user agent should load the content with the `storage-access` permission activated (i.e. load with unpartitioned cookie access, as if `document.requestStorageAccess()` had been called).
1. The user agent loads the iframe content with unpartitioned cookie access via the `storage-access` permission.
Expand Down Expand Up @@ -95,20 +95,26 @@ This is a [fetch metadata request header](https://developer.mozilla.org/en-US/do

The user agent may omit this header on same-site requests, since those requests cannot involve cross-site cookies. The user agent must include this header on cross-site requests.

If the user agent sends `Sec-Fetch-Storage-Access: inactive` on a given network request, it must also include the `Origin` header on that request.

### Response headers

```
Activate-Storage-Access: <token>
Activate-Storage-Access: retry; allowed-origin="https://foo.bar"
Activate-Storage-Access: retry; allowed-origin=*
Activate-Storage-Access: load
```
This is a [structured header](https://datatracker.ietf.org/doc/html/rfc8941) whose value is a [token](https://datatracker.ietf.org/doc/html/rfc8941#section-3.3.4) which is one of the following:
This is a [structured header](https://datatracker.ietf.org/doc/html/rfc8941) whose value is a [sf-item](https://datatracker.ietf.org/doc/html/rfc8941#section-3.3-3) (specifically a [token](https://datatracker.ietf.org/doc/html/rfc8941#section-3.3.4-3)) which is one of the following:
* `load`: the server requests that the user agent activate the `storage-access` permission before continuing with the load of the resource.
* `retry`: the server requests that the user agent activate the `storage-access` permission, then retry the request. The retried request must include the `Sec-Fetch-Storage-Access: active` header. (The user agent must ignore the token if permission is not already granted or if unpartitioned cookies are already accessible. In other words, the user agent must ignore the token if the previous request did not include the `Sec-Fetch-Storage-Access: inactive` header.)
* `retry`: the server requests that the user agent activate the `storage-access` permission, then retry the request.
* The retried request must include the `Sec-Fetch-Storage-Access: active` header. (The user agent must ignore the token if permission is not already granted or if unpartitioned cookies are already accessible. In other words, the user agent must ignore the token if the previous request did not include the `Sec-Fetch-Storage-Access: inactive` header.)
* The `retry` token must be accompanied by the `allowed-origin` [parameter](https://datatracker.ietf.org/doc/html/rfc8941#section-3.1.2-4), which specifies the request initiator that should be allowed to retry the request. (A wildcard parameter, i.e. `allowed-origin=*`, is allowed.) If the request initiator does not match the `allowed-origin` value, the user agent may ignore this header.

If the request did not include `Sec-Fetch-Storage-Access: inactive` or `Sec-Fetch-Storage-Access: active`, the user agent may ignore this header (both tokens).

If the response includes this header, the user agent may renew the `storage-access` permission associated with the request context, since this is a clear signal that the embedded site is relying on the permission.

Note: it is tempting to try to use [Critical-CH](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Critical-CH) to retry the request, but this usage would be inconsistent with existing usage and patterns for Critical-CH. The `Activate-Storage-Access: retry` header requests that the user agent _change_ some details about the request before retrying; whereas Critical-CH is designed to allow the server to request more metadata about the request, without modifying it. This proposal therefore does not rely on Critical-CH.
Note: it is tempting to try to use [Critical-CH](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Critical-CH) to retry the request, but this usage would be inconsistent with existing usage and patterns for Critical-CH. The `Activate-Storage-Access: retry; allowed-origin=<origin>` header requests that the user agent _change_ some details about the request before retrying; whereas Critical-CH is designed to allow the server to request more metadata about the request, without modifying it. This proposal therefore does not rely on Critical-CH.

## Key scenarios

Expand All @@ -128,7 +134,7 @@ Similar to the above, sites may utilize navigations to load different content or

One ability that this proposal provides is the ability for a non-iframe resource to opt into using an existing `storage-access` permission (via a header instead of JavaScript).

That ability would enable use cases like the IIIF ([cultural heritage interoperability](https://github.com/privacycg/storage-access/issues/72)) to function with a relatively minor update: each "viewer" (top-level site) needs to include an embedded iframe from the "publisher" (embedded site), perhaps on the viewer's homepage, which calls `document.requestStorageAccess()` for the publisher. Once the permission has been granted, any of the viewer's pages can include embedded `<img>` tags from the publisher. The publisher server can then use the `Activate-Storage-Access: retry` mechanism to activate the user's existing `storage-access` permission grant without the use of JavaScript, and ask the user agent to reissue the subresource request with the appropriate cross-site auth credentials.
That ability would enable use cases like the IIIF ([cultural heritage interoperability](https://github.com/privacycg/storage-access/issues/72)) to function with a relatively minor update: each "viewer" (top-level site) needs to include an embedded iframe from the "publisher" (embedded site), perhaps on the viewer's homepage, which calls `document.requestStorageAccess()` for the publisher. Once the permission has been granted, any of the viewer's pages can include embedded `<img>` tags from the publisher. The publisher server can then use the `Activate-Storage-Access: retry; allowed-origin=<origin>` mechanism to activate the user's existing `storage-access` permission grant without the use of JavaScript, and ask the user agent to reissue the subresource request with the appropriate cross-site auth credentials.

An important caveat: this proposal does not eliminate the need for a prior top-level interaction on the publisher (embedded) site, nor does it eliminate the need for _some_ call to `document.requestStorageAccess()` from a cross-site embedded iframe (or some other way to request the `storage-access` permission). Another proposal like [Top-Level Storage Access API Extension](https://github.com/bvandersloot-mozilla/top-level-storage-access) could help bridge that gap.

Expand Down Expand Up @@ -157,7 +163,7 @@ This proposal simplifies some ways in which developers can use an API that allow
The new header does expose some user-specific state in network requests which was not previously available there, namely the state of the `storage-access` permission. However, this information is not considered privacy-sensitive, for a few reasons:
* The site could have learned this information anyway by calling `navigator.permissions.query({name: 'storage-access')` and/or `document.requestStorageAccess()` in an embedded iframe.
* Note that this information is now exposed to other kinds of embedded subresources that it wasn't previously available to, however.
* The `Sec-Fetch-Storage-Access` header's value is always none unless the relevant context would be able to access unpartitioned state after calling `document.requestStorageAccess()` without triggering a user prompt. Thus, in the cases where the `Sec-Fetch-Storage-Access` header conveys interesting information, the site in question already has the ability to access unpartitioned state. So, there's no privacy benefit to omitting the `Sec-Fetch-Storage-Access` header altogether when it's not explicitly requested by `Activate-Storage-Access: retry`.
* The `Sec-Fetch-Storage-Access` header's value is always none unless the relevant context would be able to access unpartitioned state after calling `document.requestStorageAccess()` without triggering a user prompt. Thus, in the cases where the `Sec-Fetch-Storage-Access` header conveys interesting information, the site in question already has the ability to access unpartitioned state. So, there's no privacy benefit to omitting the `Sec-Fetch-Storage-Access` header altogether when it's not explicitly requested by `Activate-Storage-Access: retry; allowed-origin=<origin>`.
* Since the header only has one valid non-`active` and non-`inactive` state (namely `none`), there's no privacy benefit to omitting the `Sec-Fetch-Storage-Access` header when its value is `none`.

## Deployment considerations
Expand All @@ -167,11 +173,11 @@ Servers that begin using the `Activate-Storage-Access` header should include `Se
## Alternative designs
### Preflight requests

It is tempting to design a preflight mechanism, so that non-idempotent (or perhaps non-[simple](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests)) cross-site requests can avoid ambiguity (e.g. the server would support the request if it had just included cookies, so the server responds with the `Activate-Storage-Access: retry` header). However, this idea misinterprets the purpose of CORS preflights.
It is tempting to design a preflight mechanism, so that non-idempotent (or perhaps non-[simple](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests)) cross-site requests can avoid ambiguity (e.g. the server would support the request if it had just included cookies, so the server responds with the `Activate-Storage-Access: retry; allowed-origin=<origin>` header). However, this idea misinterprets the purpose of CORS preflights.

CORS preflights are a security mechanism, to ensure that servers which *don't* support CORS (and likely don't expect cross-origin PUT/DELETE/etc. requests) don't receive those "dangerous" requests. In other words, the preflights play the role of a handshake, after which the server has shown that it knows how to handle non-simple cross-origin requests. (Beyond the rollout of CORS and upgrades of old non-CORS-aware servers, CORS preflights still have a role in ensuring that any cross-origin request with a custom header gets preflighted for security reasons, as well.) This is important because before CORS existed, the Same Origin Policy forbade UAs from sending non-simple cross-origin requests; so servers might reasonably assume that any non-simple request they receive must be same-origin. After CORS became available, non-simple cross-origin requests were allowed by the SOP, which breaks the server's assumption *unless* those non-simple cross-origin requests are preceded by a preflight "handshake", which older servers wouldn't support (and therefore the request would fail in a safe way).
CORS preflights are a security mechanism, to ensure that servers which *don't* support CORS (and likely don't expect cross-origin PUT/DELETE/etc. requests) don't receive those "dangerous" requests. In other words, the preflights play the role of a handshake, after which the server has shown that it knows how to handle non-simple cross-origin requests. (Beyond the rollout of CORS and upgrades of old non-CORS-aware servers, CORS preflights still have a role in ensuring that any cross-origin request with a custom header gets preflighted for security reasons, as well.) This is important because before CORS existed, the Same Origin Policy forbade user agents from sending non-simple cross-origin requests; so servers might reasonably assume that any non-simple request they receive must be same-origin. After CORS became available, non-simple cross-origin requests were allowed by the SOP, which breaks the server's assumption *unless* those non-simple cross-origin requests are preceded by a preflight "handshake", which older servers wouldn't support (and therefore the request would fail in a safe way).

However, the `Sec-Fetch-Storage-Access` and `Activate-Storage-Access` headers do not enable the UA to send novel, risky requests in the same way that CORS did. The `Sec-Fetch-Storage-Access` header is purely informational; it doesn't change the properties of the request. The `Activate-Storage-Access` header allows re-inclusion of cross-site cookies, which *does* have security implications - but since not all major browsers have made third-party cookies unavailable by default, servers are already written under the assumption that incoming requests may carry cross-site cookies. Therefore, no preceding preflight "handshake" is needed as a security protection.
However, the `Sec-Fetch-Storage-Access` and `Activate-Storage-Access` headers do not enable the user agent to send novel, risky requests in the same way that CORS did. The `Sec-Fetch-Storage-Access` header is purely informational; it doesn't change the properties of the request. The `Activate-Storage-Access` header allows re-inclusion of cross-site cookies, which *does* have security implications - but since not all major browsers have made third-party cookies unavailable by default, servers are already written under the assumption that incoming requests may carry cross-site cookies. Therefore, no preceding preflight "handshake" is needed as a security protection.

### CORS integration

Expand Down

0 comments on commit f0f43c3

Please sign in to comment.