1. Introduction
The Storage Access API supports "authenticated embeds" by providing a way to request access to unpartitioned cookies in an embedded context. This currently requires an explicit call to a JavaScript API (namely requestStorageAccess()
) to:
-
Potentially prompt the user for permission; and
-
Explicitly indicate the embedded resource’s interest in using unpartitioned cookies (as a protection against CSRF attacks by an embedder).
As the above list suggests, this single API invocation is serving two orthogonal purposes:
-
It enforces a privacy boundary between the top-level site and the embedded site, and gives the user (and/or user agent) an opportunity to relax or maintain that privacy boundary.
-
It enforces a security boundary between the top-level site and the embedded site (namely, the aforementioned CSRF protection), and serves as the embedded site’s explicit signal to relax that security boundary (by allowing credentialed requests to be sent to the embedded site, in the given context).
The requirement to invoke requestStorageAccess
is therefore useful, but it imposes some challenges:
-
Use of the Storage Access API may currently require multiple network round trips and multiple resource reloads before an
iframe
can work as expected, since theiframe
must executerequestStorageAccess()
before fetching all of its embedded resources (presuming that they require access to unpartitioned cookies). In practice this means that theiframe
loads, executesrequestStorageAccess()
, then refreshes itself in order to re-do all of the embedded fetches (including unpartitioned cookies this time). -
Embedded resources currently must execute JavaScript in order to benefit from this API. This effectively means that the embedded resource must be an
iframe
that has the ability to run JavaScript, or must be a subresource fetched by such aniframe
(see the first bullet). This imposes an unnecessary burden on sites that serve access-controlled resources (i.e. resources that require authentication cookies) which are embedded in cross-site pages.
These challenges can be mitigated by supporting a new pair of headers. In particular, this document introduces:
-
`
Sec-Fetch-Storage-Access
`, a request header to convey information about whether unpartitioned cookies were included in the request, and possibly whether thestorage-access
permission has been granted. -
`
Activate-Storage-Access
`, a response header that can be used to activate an existingstorage-access
permission grant and "retry" the request, or to activate an existingstorage-access
permission prior to loading a Document (typically, aniframe
).
2. Infra
This specification depends on the Infra standard. [INFRA]
3. Storage-Access Request Infrastructure
In addition to the new headers themselves, this document introduces some new infrastructure to store and convey metadata in the user agent, particularly on a request.
A request has a boolean eligible for storage-access. It is initially false.
Note: The eligible for storage-access boolean indicates whether the user agent is allowed to include unpartitioned cookies when sending a request where the site obtained from the request’s url has a storage-access permission.
Note: A request also has a has storage access boolean, indirectly through its client. That value is distinct from the request's eligible for storage-access boolean. Both represent an "opt in" signal for accessing unpartitioned cookies in a cross-site context, but the signal comes from different origins. The request's client's has storage access field represents whether the site obtained from the environment's origin has opted in. The request's eligible for storage-access field represents whether the site obtained from the request's url's origin has opted in.
A request has an associated single-hop cache mode, whose value is null or a cache mode. It is initially set to null.
This document renames a request's cache mode field to internal cache mode.
-
If request’s single-hop cache mode is not null, return request’s single-hop cache mode.
-
Return request’s internal cache mode.
A storage access status is one of "none", "inactive", or "active".
-
If the user agent’s cookie store would attach cookies with the
SameSite=Strict
attribute to request, return null. [COOKIES] -
Let allowed be a boolean, initially set to the result of determining whether the user agent’s cookie store allows unpartitioned cookies to be accessed given request’s url, request’s client, and request’s eligible for storage-access.
-
If allowed is true, return "
active
". -
If request’s eligible for storage-access is true, return "
none
".Note: the "
storage-access
" policy-controlled feature was checked before setting request’s eligible for storage-access to true. -
Let featureIsAllowed the result of running Should request be allowed to use feature? given "
storage-access
" and request. -
If featureIsAllowed is false, return "
none
". -
Set allowed to the result of determining whether the user agent’s cookie store allows unpartitioned cookies to be accessed given request’s url, request’s client, and
true
. -
If allowed is true, return "
inactive
".Note: allowed will be true in the above step if the permission store entry obtained by getting a permission store entry given a
PermissionDescriptor
withname
initialized to "storage-access
" and a permission key of(the site obtained from request's client's top-level origin, the site obtained from request's url's origin)
has a state of "granted
". Otherwise, allowed will remain false. -
Return "
none
".
-
Let top level site be the result of obtaining a site from environment’s top-level origin.
-
Let destination site be the the result of obtaining a site from url’s origin.
-
Let key be
(top level site, destination site)
. -
Let allowed be the result of determining whether the user agent explicitly allows unpartitioned cookie access given key.
-
If allowed is true, return true.
-
Return false if all of the following conditions are false:
-
eligible for storage-access is true
-
environment’s has storage access is true and the site obtained from environment’s origin and the site obtained from url’s origin are same site
This condition ought to check a boolean that is updated after cross-site HTTP redirects. I.e., it ought to read a boolean derived from the environment’s has storage access, not that value itself. See storage-access#210.
-
-
Let entry be the result of getting a permission store entry given a
PermissionDescriptor
withname
initialized to "storage-access
" and a permission key of key. -
Return true.
4. Storage-Access Headers
The following sections define a request header and a response header. The request header exposes information about the request's access to cookies to a server. The response header allows a server to opt into accessing unpartitioned cookies on a particular request or when loading an iframe
.
4.1. The Sec-Fetch-Storage-Access
HTTP Request Header
The Sec-Fetch-Storage-Access
HTTP request header exposes a request's
ability or inability to access cookies to a server. It is a Structured Field item whose value MUST be a token. [I-D.structured-field-values-for-http] Its ABNF is:
Sec-Fetch-Storage-Access = sf-token
Valid Sec-Fetch-Storage-Access
values include "none
", "inactive
", and
"active
". In order to support forward-compatibility with as-yet-unknown
semantics, servers SHOULD ignore this header if it contains an invalid value.
// When the request’s credentials mode is "omit
", the header is omitted:// When the request is same-site, the header is omitted: // When the request has no access to unpartitioned cookies, the header’s value is " [=storage access status/none=]
": Sec-Fetch-Storage-Access: none // When the request has no access to unpartitioned cookies, but // 'storage-access' permission has already been granted, the header’s value is // "[=storage access status/inactive=]
": Sec-Fetch-Storage-Access: inactive // When the request has access to unpartitioned cookies, the header’s value is "[=storage access status/active=]
": Sec-Fetch-Storage-Access: active
Sec-Fetch-Storage-Access
header for a request request:
-
Assert: request’s url is a potentially trustworthy URL.
-
If request’s credentials mode is not "
include
", abort these steps. -
Let access be request’s storage access status.
-
If access is null, abort these steps.
-
Let value be a Structured Field value whose value is a token.
-
Set value’s value to access.
-
Set a structured field value given ("
Sec-Fetch-Storage-Access
", value) in request’s header list.
4.2. The Activate-Storage-Access
HTTP Response Header
The Activate-Storage-Access
HTTP response header
allows a server to opt in to accessing its unpartitioned cookies in a cross-site request
context. It is a Structured Field item whose value MUST be a token. [I-D.structured-field-values-for-http] Its ABNF is:
Activate-Storage-Access = sf-item
Valid Activate-Storage-Access
values include "load
" and "retry
".
The following parameter is defined:
-
A parameter whose key is "
allowed-origin
", and whose value is a string. See below for processing requirements.
// The server’s response requests that the user agent activate storage access // before continuing with the load of the resource. (This is only relevant when // loading a new document.) Activate-Storage-Access: load // The server’s response requests that the user agent activate storage access, // then retry the request. The "allowed-origin" parameter allowlists the // request’s origin. Activate-Storage-Access: retry; allowed-origin="https://foo.bar" // Same as above, but using a wildcard instead of explicitly naming the request’s origin. Activate-Storage-Access: retry; allowed-origin=*
-
If request’s credentials mode is not "
include
", return failure. -
If request’s eligible for storage-access is true, return failure.
-
Let storageAccessStatus be request’s storage access status.
-
If storageAccessStatus is not "
inactive
", return failure. -
Let parsedHeader be the result of getting a structured field value given "
Activate-Storage-Access
" and "item
" from response’s header list. -
If parsedHeader is null, return failure.
-
Let (value, params) be parsedHeader.
-
If value is not a token, return failure.
-
If value’s value is not "
retry
", return failure. -
If params["allowed-origin"] does not exist, return failure.
-
Let allowedOrigin be params["allowed-origin"].
-
If allowedOrigin is a token whose value is "
*
", return success. -
If allowedOrigin is not a string, return failure.
-
If the result of byte-serializing a request origin with request is not allowedOrigin’s value, then return failure.
-
Return success.
-
Let storageAccessStatus be request’s storage access status.
-
If storageAccessStatus is not one of "
inactive
" or "active
", return failure. -
Let parsedHeader be the result of getting a structured field value given "
Activate-Storage-Access
" and "item
" from response’s header list. -
If parsedHeader is null, return failure.
-
Let (value, params) be parsedHeader.
-
If value is not a token, return failure.
-
If value’s value is not "
load
", return failure. -
Return success.
5. Integration with Fetch Metadata
The `Sec-Fetch-Storage-Access
` header is appended to outgoing requests alongside other Fetch Metadata headers. [FETCH-METADATA] Modify the definition of append the Fetch metadata headers for a request by inserting the following as step 6:
6. Integration with Fetch
Handling these headers requires modifications to a few different parts of Fetch. [FETCH]
6.1. Origin
header
When making a decision on whether to retry a request and force it to include unpartitioned cookies, a server ought to be informed as to the initiator of the request. I.e., the request ought to include the `Origin
` header whenever it also includes the Sec-Fetch-Storage-Access: inactive
header. Modify the definition of append a request Origin header by rewriting step 4 as:
-
If at least one of the following conditions is true:
-
the result of getting `
Sec-Fetch-Storage-Access
` from request’s header list is "inactive
" -
request’s method is neither `
GET
` nor `HEAD
`
Then:
-
The rest of the algorithm is unmodified.
6.2. HTTP-fetch
Insert a new step after step 5 in HTTP fetch:
-
If the result of performing a storage access retry check for request is success, then return the result of running HTTP-storage-access-retry-fetch given fetchParams.
Insert a new step after step 6.1 (before "switch on request’s redirect mode"):
-
Set request’s single-hop cache mode to null.
The rest of the algorithm is unmodified.
-
Let request be fetchParams’s request.
-
Assert: request’s storage access status is "
inactive
". -
Assert: request’s eligible for storage-access is false.
-
If request’s redirect count is 20, then return a network error.
-
Increase request’s redirect count by 1.
-
Set request’s single-hop cache mode to "
reload
". -
Set request’s eligible for storage-access to true.
-
Assert: request’s storage access status is "
active
". -
Let recursive be true.
-
Return the result of running main fetch given fetchParams and recursive.
6.3. HTTP-redirect-fetch
Insert a new step after step 17 in HTTP-redirect fetch:
-
If locationURL’s origin is not same origin with request’s url's origin, set request’s eligible for storage-access to false.
The rest of the algorithm is unmodified.
7. Integration with HTML
7.1. Changes to navigation
This integration builds upon the changes introduced by the Storage Access API specification. [STORAGE-ACCESS]
In particular, modify the changes when creating the request’s reserved client in create navigation params by fetching to be the following:
-
Let compute has storage access be an algorithm with the following steps, which return a boolean:
-
If response is not null and the result of performing a storage access load check given request and response is success, return true.
-
If sourceSnapshotParams’s environment id does not equal navigable’s active document's relevant settings object's id, return false.
-
If originalURL’s origin is not same origin with currentURL’s origin, return false.
-
If response is not null and response’s has-cross-origin-redirects is true, return false.
-
Return true.
-
-
Set request’s reserved client's has storage access to the result of executing compute has storage access.
8. Security Considerations
8.1. Opt-In signal
The primary security concerns for this specification are those laid out in privacycg/storage-access#113. Namely: since the Storage Access API makes unpartitioned 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 unpartitioned 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 unpartitioned cookies by invokingrequestStorageAccess()
.
Storage Access Headers continues in the same vein by requiring embedded cross-site resources (or rather, their servers) to explicitly opt-in to accessing unpartitioned cookies (by supplying an HTTP response header), before any unpartitioned cookies are included on the request. When a server opts in by sending the Activate-Storage-Access: retry
header, it also must explicitly name the origin that it grants the ability to send credentialed requests (via the "allowed-origin
" parameter). This fails closed by blocking credentialed requests, in the event of an origin mismatch.
8.2. Forbidden header name
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 unpartitioned 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.
8.3. Deeper CORS Integration
It is tempting to design this specification such that it piggy-backs and/or integrates with CORS (i.e., the CORS protocol) deeply, 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:
-
If CORS (and the relevant SAA permission, of course) were a "sufficient" condition for attaching unpartitioned cookies...
-
Then this would allow the top-level site to attack the embedded site by sending (CORS-enabled) credentialed requests to arbitrary endpoints on the embedded site, without requiring any opt-in from the embedded site before it received those requests. This would make CSRF attacks against the embedded site more feasible. This is undesirable for security reasons.
-
-
If CORS were required for the user agent to attach unpartitioned cookies to the request...
-
Then this would mean the embedded site would be required to allow the top-level site to read the bytes of its responses and response headers, just so that the user agent would include cookies when fetching the embedded resource. This is a more powerful capability than simply attaching unpartitioned cookies, so this would expose the embedded site to unnecessary attack vectors from the top-level site. This is undesirable for security reasons.
-
This would also mean that in order to fix an embedded widget on some page, the top-level site must perform some action to enable CORS; the embedded site alone would be unable to update the page and fix the widget. This is undesirable from a developer usability / composability standpoint.
-
Therefore, CORS ought to be neither necessary nor sufficient for attaching unpartitioned cookies to a cross-site request. This specification is therefore designed to be orthogonal to CORS.
Note: This specification does rely on the `Origin
` header, which is defined by CORS, so this specification does integrate with CORS in a technical sense. This is intentional, since in that case we are able to reuse an existing header that sends exactly the information that this specification needs, and both the new usage and existing usage are for security features.
9. Privacy Considerations
This specification simplifies some ways in which developers can use an API that allows access to unpartitioned data. However, it does not meaningfully change the privacy characteristics of the Storage Access API [STORAGE-ACCESS]: sites are still able to ask for the ability to access unpartitioned cookies; user agents are still able to handle those requests how they see fit. Importantly, if the storage-access
permission is not granted by the user or user agent, then this specification does not allow use of unpartitioned data.
The `Sec-Fetch-Storage-Access
` 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 embedded site could have learned this information anyway by calling
query
and/orrequestStorageAccess()
in an embedded iframe. These APIs are not treated as privacy-sensitive. -
The `
Sec-Fetch-Storage-Access
` header’s value is "none
" unless the relevant context would be able to access unpartitioned state after callingrequestStorageAccess()
without triggering a user prompt. Thus, in the cases where the `Sec-Fetch-Storage-Access
` header conveys interesting information (i.e. "inactive
" or "active
"), the site in question already has the ability to access unpartitioned state, by assumption. So, there is zero privacy benefit to omitting the `Sec-Fetch-Storage-Access
` header altogether in those cases.-
Conversely, since the `
Sec-Fetch-Storage-Access
` 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 neither "inactive
" nor "active
".
-
10. Deployment Considerations
10.1. Vary
If a given endpoint might use the `Activate-Storage-Access
` header, then developers should include `Sec-Fetch-Storage-Access
` in the response’s Vary
header [RFC9110], to ensure that caches handle the response appropriately. For example, Vary: Accept-Encoding, Sec-Fetch-Storage-Access
.
10.2. Origin
Header Interoperability
Some servers misbehave if they receive the `Origin
` header when they weren’t expecting to. However, the `Origin
` header conveys exactly the information that a server would need before making an informed choice on whether to respond with the Activate-Storage-Access: retry
header, so it’s a perfect candidate for reuse by this specification (rather than inventing some new Origin2
header). This specification strives to minimize new breakage due to including the `Origin
` header on more requests, by minimizing the set of requests that newly include the `Origin
` header. In particular, the `Origin
` header is only (newly) included on cross-site requests whose storage access status is "inactive
" and whose credentials mode is "include
".
10.3. Sec-
Prefix
The `Sec-Fetch-Storage-Access
` header’s name is prefixed with Sec-
because only the user agent is permitted to set such headers (as they are forbidden request-headers). Therefore, the `Sec-Fetch-Storage-Access
` name is guaranteed to not conflict with any preexisting headers in use on the web.
10.4. Header Compression
The `Sec-Fetch-Storage-Access
` header has exactly 3 legal values. Therefore it should perform well with HPACK, per [MNOT-DESIGNING-HEADERS]. Optimizing the length of the header name has little impact compared to minimizing the number of legal header values.
The `Activate-Storage-Access
` header has an unbounded number of legal values, but only a small number of them (perhaps 1-2) can reasonably be expected to occur in a single HTTP connection. This header should therefore also perform reasonably well with HPACK.
11. IANA Considerations
The permanent message header field registry should be updated with the following registrations for the headers defined in this specification: [RFC3864]
11.1. Sec-Fetch-Storage-Access
Registration
- Header field name
-
Sec-Fetch-Storage-Access
- Applicable protocol
-
http
- Status
-
draft
- Author/Change controller
-
Me
- Specification document
-
This specification (See § 4.1 The Sec-Fetch-Storage-Access HTTP Request Header)
11.2. Activate-Storage-Access
Registration
- Header field name
-
Activate-Storage-Access
- Applicable protocol
-
http
- Status
-
draft
- Author/Change controller
-
Me
- Specification document
-
This specification (See § 4.2 The Activate-Storage-Access HTTP Response Header)
12. Acknowledgements
Thanks to Johann Hofmann, Artur Janc, Ben VanderSloot, Dom Farolino, Matt Menke, Adam Rice, and Maks Orlovich, who all provided valuable insight and support in the design of this mechanism.