storage-access
permission already (but has not opted in yet).storage-access
permission is not a goal.The Storage Access API supports "authenticated embeds" by providing a way to opt in to accessing unpartitioned cookies in an embedded context. The API currently requires an explicit call to a JavaScript API to 1) potentially prompt the user for permission, and 2) explicitly indicate the embedded resource's interest in using unpartitioned cookies (as a protection against CSRF attacks by an embedder).
This requirement is unacceptable for some authenticated embed use cases, and imposes a cost on even the well-suited use cases after they have obtained permission:
These costs and constraints can be avoided by supporting a few new headers.
<iframe>
As an illustrative example, consider a calendar widget on calendar.com, embedded in example.com. During the user's first-ever visit to the example.com page, the flow of events is the following:
document.requestStorageAccess()
.
storage-access
permission has been granted.
This is working as intended, since the user agent may choose to delegate the decision to grant storage-access
permission to the user, and the user ought to have the benefit of context for that decision.
However, consider a subsequent visit to the example.com page, after the storage-access
permission has already been granted by the user or user agent. Without this proposal, the flow on the subsequent visit looks exactly the same as the flow on the first visit. However, the user does not need to grant permission this time, since they have already granted permission. This means that the latency and network traffic incurred by the first iframe load, the document.requestStorageAccess()
script execution, and the subsequent reload are entirely unnecessary.
Instead, we can imagine a different flow, where the user agent recognizes that the calendar widget already has storage-access
permission and somehow knows that the widget wants to opt in to using it, so it loads the iframe with access to unpartitioned cookies. This would avoid unnecessary latency and power drain due to network traffic and script execution, leading to a better user experience. So, the flow could be:
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.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.storage-access
permission for this fetch).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).storage-access
permission.
This flow avoids loading the widget twice, and avoids executing script solely for the document.requestStorageAccess()
call to activate the existing permission grant. It also avoids the network transmission of the "placeholder" version of the widget.
Additionally, the use of HTTP headers removes the requirement for JavaScript execution. This enables non-iframe resources to take full advantage of existing storage-access
permission grants.
<iframe>
Consider a document that includes an image (e.g.) which happens to be served by a different (unrelated) site.
At present, no web platform API allows loading this image via a credentialed fetch in browsers that block third-party cookies by default. So, if the image requires the user's credentials (i.e. unpartitioned cookies), then this is broken.
However, if the browser supports the headers described below (and if the user has already granted the storage-access
permission to the appropriate <site, site>
pair somehow - e.g. via an iframe at some point in the recent past), then this scenario is supported by the browser as in the following sequence:
Browsers that do not support the proposed headers will still receive the appropriate 401 Unauthorized
response. However, browsers that do support the proposed headers are able to retry the fetch and can send the user's credentials, since the user has already given permission for this (by assumption).
Sec-Fetch-Storage-Access: <access-status>
This is a fetch metadata request header (with a forbidden header name), where the <access-status>
directive is one of the following:
none
: the fetch's context does not have access to unpartitioned cookies, and does not have the storage-access
permission.inactive
: the fetch's context has the storage-access
permission, but has not opted into using it; and does not have unpartitioned cookie access through some other means.active
: the fetch's context has unpartitioned cookie access.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.
Activate-Storage-Access: retry; allowed-origin="https://foo.bar"
Activate-Storage-Access: retry; allowed-origin=*
Activate-Storage-Access: load
This is a structured header whose value is a sf-item (specifically a token) 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.
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
token must be accompanied by the allowed-origin
parameter, 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 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.
Relative to the Storage Access API's current specification, this proposal allows the user agent to elide some unnecessary network traffic, resource loads, and script execution when a user repeatedly visits a site with an authenticated embed. This results in a few benefits:
Similar to the above, sites may utilize navigations to load different content or as mechanisms to authenticate users. For example, a site might want to preserve storage access status in its embeds while the user visits different top-level pages.
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) 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 could help bridge that gap.
User agents that do not support these headers, or do not wish to allow header-based opt-in, do not have to send the Sec-Fetch-Storage-Access
header at all; servers should interpret this as equivalent to Sec-Fetch-Storage-Access: none
, in which case scripts will need to call document.requestStorageAccess()
before cross-site cookies can become available. The Storage Access API does not rely on support for these headers.
Importantly, this proposal does not introduce a new mechanism to request storage access when an embed has not previously obtained permission, and so website developers must still implement the existing JS-based permission request flow (usually via document.requestStorageAccess()
) to handle cases where storage access is not granted (or the browser does not reveal whether it is granted).
The biggest security concerns to keep in mind for this proposal are those laid out in https://github.com/privacycg/storage-access/issues/113. Namely: since the Storage Access API makes cross-site cookies available even after those cookies have been blocked by default, it is crucial that the Storage Access API not preserve the security concerns traditionally associated with cross-site cookies, like CSRF.
The principal way that the Storage Access API addresses these security concerns is by requiring an embedded cross-site resource (e.g. an iframe) to explicitly opt in to accessing cross-site cookies by calling a JavaScript API. This proposal continues in that vein by requiring embedded cross-site resources (or their servers) to explicitly opt-in to accessing cross-site cookies (by supplying an HTTP response header).
This proposal uses a new forbidden name for the Sec-Fetch-Storage-Access
header to prevent programmatic modification of the header value. This is primarily for reasons of coherence, rather than security, but there is a security reason to make this choice. If a script could modify the value of the header, it could lie to a server about the state of the storage-access
permission in the requesting context and indicate that the state is active
, even if the requesting context has not opted in to using the permission grant. This could mislead the server into inferring that the request context is more trusted/safe than it actually is (e.g., perhaps the requesting context has intentionally not opted into accessing its cross-site cookies because it cannot conclude it's safe to do so). This could lead the server to make different decisions than it would have if it had received the correct header value (none
or inactive
). Thus the value of this header ought to be trustworthy, so it ought to be up to the user agent to set it.
This proposal simplifies some ways in which developers can use an API that allows access to cross-site data. However, it does not meaningfully change the privacy characteristics of the Storage Access API: sites are still able to ask for the ability to access cross-site cookies; user agents are still able to handle those requests how they see fit.
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:
navigator.permissions.query({name: 'storage-access')
and/or document.requestStorageAccess()
in an embedded iframe.
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>
.
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
.Servers that begin using the Activate-Storage-Access
header should include Sec-Fetch-Storage-Access
in the response's Vary header. This prevents user agents from receiving fallback content for requests that included Sec-Fetch-Storage-Access: active
.
It is tempting to design a preflight mechanism, so that non-idempotent (or perhaps non-simple) 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 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 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.
It is tempting to design this functionality such that it piggy-backs and/or integrates with CORS directly, since CORS intuitively feels like it is meant to address a similar problem of enabling cross-origin functionality. However, this would be undesirable for a few reasons:
Therefore, CORS ought to be neither necessary nor sufficient for attaching unpartitioned cookies to a cross-site request. We will therefore design the unpartitioned-cookies-opt-in mechanism as a new thing, completely indepedent from CORS.
The existing Storage Access API specification and discussions in its GitHub issues heavily inspired this document.