1. Introduction
This section is non-normative.
Storage is normally partitioned by origin to protect user security and privacy. That is the right default, but it is a poor fit for a narrow class of very large, highly popular, publicly-distributed resources — AI models, WebAssembly modules, JavaScript libraries, game engines, and large web fonts — that are, byte-for-byte, the same file no matter which site requested them. When two unrelated origins each depend on the same 8 GB model, origin-partitioned storage forces the user to download and retain that model twice, which is wasteful for the user’s bandwidth, storage, and battery, and for the network as a whole.
Cross-Origin Storage (COS) is a content-addressable cache, keyed by cryptographic hash rather than by URL, that lets a user agent share a single stored copy of such a resource across the origins that opt into using it. Storing a resource in COS is always an explicit, opt-in act by the storing origin, and a user agent may withhold confirming a resource’s presence even from origins that would otherwise be permitted to read it; see § 7 Privacy and security considerations.
The entry point for this specification is the requestFileHandle()
method, exposed via crossOriginStorage:
const hash= { algorithm: 'SHA-256' , value: '8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4' , }; try { const handle= await navigator. crossOriginStorage. requestFileHandle( hash); const file= await handle. getFile(); // Do something with the file. } catch ( err) { if ( err. name=== 'NotFoundError' ) { // Not (disclosably) in Cross-Origin Storage; fetch from the network instead. } }
This specification reuses FileSystemFileHandle, FileSystemWritableFileStream, and related
infrastructure from the File System Standard [FS], scoped to a dedicated, cross-origin-shared
file system rather than to a single origin’s private or user-visible file system.
This specification defines the imperative JavaScript API only. Related proposals integrate the
same underlying cache with declarative markup — an HTML crossoriginstorage attribute on
link and script, a crossOriginStorage JavaScript import attribute, and a CSS
cross-origin-storage() <request-url-modifier> — each defined in its host language’s own
specification; see § 8 Integration with other specifications.
2. Concepts
2.1. Hashes
A COS hash is a struct with the following items:
- algorithm
-
A string naming a hash algorithm recognized by [WEBCRYPTO], for example "
SHA-256". - value
-
A string that is a lowercase hexadecimal encoding of a digest produced by algorithm. For "
SHA-256", this is 64 characters long.
The algorithm dictionary member is typed as a plain
DOMString, not as a HashAlgorithmIdentifier, even though its value space is exactly the set
of names accepted by [WEBCRYPTO]’s hash algorithms. A HashAlgorithmIdentifier is
(object or DOMString) — it additionally admits an object, normalized by [WEBCRYPTO] into an
Algorithm-shaped dictionary, so that callers can pass extra parameters to algorithms that need
them (for example, {name: "HMAC", hash: "SHA-256"}). Hash algorithms take no such parameters,
algorithm is stored, compared, and round-tripped as part of a content-addressable
key rather than consumed once by a single operation, and every use in this specification’s
examples is a bare string; admitting arbitrary objects here would add no capability while making
equality and serialization needlessly complicated. DOMString is therefore the
narrower and more correct type for this field.
Two COS hash values are equal if their algorithm values are an ASCII case-insensitive match and their value values are exactly equal.
Note: Unlike algorithm, whose case is not normatively constrained, value is already normatively lowercase (see above), so comparing it is a plain string comparison rather than an ASCII case-insensitive one.
A content-addressable store is one whose entries are keyed by the equality of a COS hash, rather than by URL or by name: two files with identical bytes and the same hash algorithm are the same entry, regardless of how many origins stored them or how many URLs they were fetched from.
2.2. COS entries
Each user agent has a single COS registry, a map from COS hash values to COS entry structs, shared by all origins that use Cross-Origin Storage. A COS entry has the following items:
- hash
-
A COS hash.
- bytes
-
Null, or a byte sequence whose digest under hash’s algorithm equals hash’s value. Null until a writer completes verification and storage.
- state
-
Either "
pending" or "written". An entry starts out "pending" and becomes "written" exactly once, when bytes is first set. - origins
-
The declared sharing scope: either "
*" (any origin), a list of origin strings (only those origins), or null (same-site origins only). Set by the first writer and upgradeable but never downgradeable. - storing origins
-
A set of origins that have each successfully completed writing this entry’s bytes at least once. Persisted across page loads and grows over time; never shrinks except as described in § 5.2 Eviction. An origin in storing origins may always obtain a handle for the entry via
requestFileHandle(), independent of origins or whether the entry’s hash is on the Public Hash List (PHL). See § 3.4.1 Original storer access.
A user agent has an associated Cross-Origin Storage queue, which is the result of starting a new parallel queue. All operations on the COS registry are to be enqueued on this queue, so that they execute in the order they were enqueued and do not interleave.
2.3. The Public Hash List
Confirming that a hash is present in Cross-Origin Storage can itself leak information about a
user’s browsing history (see § 7.2 Cross-site probing). This is a risk specifically for globally
scoped entries — an entry whose origins is a list or null has already had its
disclosure bounded by an explicit choice of the storing origin, but "*" potentially exposes an
entry to any origin on the web. To bound that specific risk, a "*"-scoped resource is only
disclosable to origins outside its storing origins if its hash clears an additional,
independent gate: membership on the Public Hash List
(PHL), a vendor-neutral, implementation-defined allowlist of COS hash
values. A hash is admitted to the Public Hash List only once it clears a k-anonymity-style
popularity bar — for example, appearing, byte-identical, across some minimum number of independent
origins — so that confirming its presence in a shared cache is not informative about any
individual user.
A COS hash hash is on the Public Hash List if hash equals an entry of the user agent’s current snapshot of the Public Hash List.
The retrieval protocol, update cadence, data format, popularity thresholds, and governance of the Public Hash List are designed in detail as a companion artifact to this specification, in the spirit of how the [URL] Standard’s public suffix list is a companion data file rather than normative prose — see the Public Hash List explainer in this repository. That design proposes governance by the WHATWG, modeled on the Public Suffix List’s cross-vendor, rolling-release precedent, and admission criteria keyed on independently corroborated ubiquity —a concrete instance of the k-anonymity-style bar described above, applied once, offline, as part of how a hash is admitted, not as a check the user agent repeats per query. None of this is yet established outside this proposal. An early, non-normative code prototype of the list itself is available at tomayac/public-hash-list. This specification only depends on whether a hash is on the PHL, not on any particular retrieval mechanism or governance model, so it remains correct regardless of how that design evolves.
3. The CrossOriginStorageManager interface
[Exposed =(Window ,Worker ),SecureContext ]interface {CrossOriginStorageManager Promise <FileSystemFileHandle >requestFileHandle (CrossOriginStorageRequestFileHandleHash ,hash optional CrossOriginStorageRequestFileHandleOptions = {}); };options dictionary {CrossOriginStorageRequestFileHandleHash required DOMString ;value required DOMString ; };algorithm dictionary {CrossOriginStorageRequestFileHandleOptions boolean =create false ; (DOMString or sequence <DOMString >); };origins interface mixin { [NavigatorCrossOriginStorage SameObject ,SecureContext ]readonly attribute CrossOriginStorageManager crossOriginStorage ; };Navigator includes NavigatorCrossOriginStorage ;WorkerNavigator includes NavigatorCrossOriginStorage ;
Each Navigator and WorkerNavigator object has an associated CrossOriginStorageManager
object. The crossOriginStorage getter steps
are to return this’s associated CrossOriginStorageManager.
Each CrossOriginStorageManager object has an associated origin, set to
this’s relevant settings object’s origin when the object
is created.
3.1. The requestFileHandle() method
- handle = await navigator . crossOriginStorage .
requestFileHandle(hash) -
Returns a handle for the file identified by hash, if it is present in, and disclosable from, Cross-Origin Storage to the calling origin. Otherwise, this rejects with a "
NotFoundError"DOMException. A "NotFoundError" does not prove the file is physically absent from Cross-Origin Storage; see § 7.3 Availability gating. Callers can treat it as "fetch this from the network instead." - handle = await navigator . crossOriginStorage .
requestFileHandle(hash, {create: true }) -
Returns a handle that can be used to write the file identified by hash into Cross-Origin Storage, creating a new entry if none exists yet, restricted by default to same-site origins. The caller needs to write the complete file contents via the handle’s
createWritable()method regardless of whether an entry already existed; the user agent verifies at that point that the written bytes hash to hash, rejecting with a "DataError"DOMExceptionotherwise. This requirement prevents an origin from using a create request as an oracle for whether hash was already present. - handle = await navigator . crossOriginStorage .
requestFileHandle(hash, {create: true,origins: "*" }) -
As above, additionally making the entry disclosable to any origin whose request for hash clears § 7.3 Availability gating, once written.
- handle = await navigator . crossOriginStorage .
requestFileHandle(hash, {create: true,origins: ["https://a.example", "https://b.example"] }) -
As above, restricting disclosure to exactly the listed origins (in addition to the calling origin, and any origin already in storing origins).
requestFileHandle(hash, options) method
steps are:
-
Let result be a new promise.
-
Let realm be this’s relevant Realm.
-
Let global be this’s relevant global object.
-
If global’s associated
Document, if any, is not allowed to use the "cross-origin-storage" policy-controlled feature, then:-
Queue a global task on the DOM manipulation task source given global to reject result with a "
NotAllowedError"DOMException. -
Return result.
-
-
Let validationFailure be the result of running validate a COS request given hash and options.
-
If validationFailure is not null:
-
Queue a global task on the DOM manipulation task source given global to reject result with validationFailure.
-
Return result.
-
-
Enqueue the following steps to the Cross-Origin Storage queue:
-
If options["
create"] is true:-
Run complete a create request given result, hash, options, global, and realm.
-
-
Otherwise:
-
Run complete a read request given result, hash, origin, global, and realm.
-
-
-
Return result.
CrossOriginStorageRequestFileHandleHash hash
and a CrossOriginStorageRequestFileHandleOptions options, return null or a TypeError:
-
If hash["
algorithm"] is not a hash algorithm name recognized by [WEBCRYPTO], return a newTypeError. -
If hash["
value"] does not match the regular expression/^[0-9a-f]{64}$/when hash["algorithm"] is an ASCII case-insensitive match for "SHA-256", return a newTypeError.Note: Future hash algorithms can define a different expected digest length; this specification only normatively constrains "
SHA-256", matching its use throughout the examples. -
If options["
origins"] exists and is not "*":-
Let candidates be options["
origins"], with a single string treated as a list of one. -
If candidates’s size is greater than the user agent’s maximum origins list length, return a new
TypeError. -
For each candidate of candidates:
-
If the result of running the basic URL parser on candidate is failure, or if the parsed URL’s origin is an opaque origin, return a new
TypeError.
-
-
-
Return null.
3.2. Reading files
-
Let entry be the COS entry in the COS registry whose hash equals hash, if any, or null otherwise.
-
If entry is not null and entry’s state is "
pending":-
Queue a global task on the DOM manipulation task source given global to reject result with a "
NotAllowedError"DOMException. -
Return.
Note: A hash with a write in progress deliberately does not behave like a hash that is absent, so that a caller does not mistake an in-progress write for a genuine cache miss and begin a redundant, concurrent download of a very large file for the sole purpose of writing it. See § 3.3 Creating and writing files.
-
-
Let disclosableEntry be the result of running apply availability gating given entry and origin.
-
If disclosableEntry is null:
-
Queue a global task on the DOM manipulation task source given global to reject result with a "
NotFoundError"DOMException. -
Return.
-
-
Let handle be the result of creating a new
FileSystemFileHandle[FS] whose locator addresses disclosableEntry within the Cross-Origin Storage file system, in realm. -
Queue a global task on the DOM manipulation task source given global to resolve result with handle.
-
If entry is null, return null.
-
If origin is in entry’s storing origins, return entry.
Note: The original storer, and any origin that has itself successfully written the entry, can always read it back. See § 3.4.1 Original storer access.
-
If entry’s origins is "
*":-
If entry’s hash is not on the PHL, return null.
Note: The Public Hash List gate applies only here, to globally-scoped entries — the one case where disclosure could otherwise reach any origin on the web. It does not apply to the list- and same-site-scoped cases below: a storing origin has already made an explicit, bounded disclosure decision for those, and requiring separate global ubiquity on top of it would make ordinary restricted sharing (see § 3.4 Resource visibility upgrades) depend on unrelated, public curation of what is often a proprietary resource. See § 7.2 Cross-site probing.
-
Return entry.
-
-
-
If origin’s serialization is an item of that list, return entry.
-
Return null.
-
-
If origin is same site with some item of entry’s storing origins, return entry.
-
Return null.
-
Let disclosable be the result of running determine COS disclosure given entry and origin.
-
If disclosable is null, return null.
-
If origin is in disclosable’s storing origins, return disclosable.
-
If the user agent elects to apply GREASE’ing to this request, return null.
-
Return disclosable.
3.3. Creating and writing files
A FileSystemFileHandle created by complete a create request has an associated
requested origins, the value that verify and store will
attempt to upgrade the entry’s origins to once the
handle is successfully written through.
CrossOriginStorageRequestFileHandleOptions options, a global object
global, and a Realm realm:
-
Let requestedOrigins be the result of running normalize requested origins given options["
origins"]. -
Let entry be the COS entry in the COS registry whose hash equals hash, if any, or null otherwise.
-
If entry is null:
-
Set entry to a new COS entry whose hash is hash, bytes is null, state is "
pending", origins is requestedOrigins, and storing origins is an empty set. -
Set the COS registry[hash] to entry.
-
-
Let handle be the result of creating a new
FileSystemFileHandle[FS] whose locator addresses entry within the Cross-Origin Storage file system, in realm. -
Set handle’s requested origins to requestedOrigins.
Note: handle is returned whether or not entry already existed, and whether or not it is already "
written". The caller still needs to supply the complete file bytes through handle, so that a write cannot be used to detect prior presence (see § 7.2 Cross-site probing) and so that a request for a more permissive origins value can be verified before being honored; see § 3.4 Resource visibility upgrades. -
Queue a global task on the DOM manipulation task source given global to resolve result with handle.
*", a list of origins, or null:
-
If origins does not exist, return null.
-
If origins is "
*", return "*". -
Let list be « ».
-
For each candidate of origins (a single string is treated as a list of one):
-
Let candidateOrigin be the origin resulting from running the basic URL parser on candidate.
Note: validate a COS request has already confirmed each candidate parses to a non-opaque origin.
-
If list does not contain candidateOrigin, append candidateOrigin to list.
-
-
Return list.
Note: Deduplicating here, rather than only when later merging into an existing entry, keeps origins duplicate-free from the moment an entry is first created, so every later clone of it stays duplicate-free too.
When the FileSystemWritableFileStream obtained by calling
createWritable() on a handle whose locator
addresses a COS entry is closed [FS], the user agent must run the verify and store steps
below before that closing operation’s promise is fulfilled.
Note: This applies regardless of how closing was triggered — an explicit
close() call, or a pipeTo() call that reaches the end of its
source, which by default also closes its destination. [STREAMS] does not distinguish the two at
the point a stream becomes closed, and neither does this algorithm.
-
Let computedValue be the lowercase hexadecimal digest of bytes, computed using the algorithm named by entry’s algorithm, per [WEBCRYPTO].
-
If computedValue is not exactly equal to entry’s value:
-
Reject the closing operation’s promise with a "
DataError"DOMException, leaving entry unmodified. -
Abort these steps.
-
-
Enqueue the following steps to the Cross-Origin Storage queue:
-
Set entry’s bytes to bytes.
-
Set entry’s state to "
written". -
Append origin to entry’s storing origins, if not already present.
-
Run upgrade resource visibility given entry and the handle’s requested origins.
-
The File System Standard [FS] does not yet define an extension point for a dependent
specification to hook additional per-write validation into FileSystemWritableFileStream’s
closing steps. Until such a hook exists, this section describes the required behavior directly; a
future revision is expected to formally integrate with [FS].
3.4. Resource visibility upgrades
The visibility of a COS entry can be upgraded but never downgraded.
*", a list of
origins, or null requestedOrigins:
-
If requestedOrigins is null, return.
Note: Omitting
originsnever narrows an existing entry; it only requests same-site availability, which every entry already has at least as much of. -
If entry’s origins is "
*", return.Note: An already-globally-available entry cannot be restricted by a later writer. When this note applies and requestedOrigins is not "
*", the user agent is expected to log a console warning informing the developer that the requested restriction was not applied. -
If requestedOrigins is "
*":-
Set entry’s origins to "
*". -
Return.
-
-
If entry’s origins is null:
-
Set entry’s origins to requestedOrigins.
-
Return.
-
-
For each candidateOrigin of requestedOrigins:
-
If merged contains candidateOrigin, then continue.
-
If merged’s size equals the user agent’s maximum origins list length, then break.
Note: Any remaining candidateOrigin values are silently dropped from this upgrade rather than failing it — the write itself already succeeded — but the user agent is expected to log a console warning that the entry’s origins list is at capacity.
-
append candidateOrigin to merged.
-
-
Set entry’s origins to merged.
Note: A new site — not only the original storer — can widen an entry’s origins, as long as it also supplies bytes that hash to the entry’s hash. This is intentional: any site that already possesses the correct bytes for a hash is, by construction, as authoritative as the original storer about what that hash denotes.
3.4.1. Original storer access
An origin that has successfully completed writing a COS entry — that
is, any origin in the entry’s storing origins — can always subsequently obtain
a handle for it via requestFileHandle(), independent of the entry’s
origins value and independent of whether its hash is on the PHL.
This mirrors the Cache API’s model, in which an origin always has access to what it stored.
4. The Cross-Origin Storage file system
The Cross-Origin Storage file system is a file system root, distinct from any origin’s bucket file system or local file system access roots, whose entries correspond one-to-one with the COS registry’s COS entry items.
The Cross-Origin Storage file system does not use [FS]’s ordinary per-call permission-check
model: every FileSystemFileHandle obtained from it has already been fully authorized by
§ 3.2 Reading files or § 3.3 Creating and writing files before it is returned to script, so calling
getFile() or createWritable() on such a handle
never triggers an additional permission prompt.
A FileSystemFileHandle addressing a COS entry entry can exist while entry’s
state is still "pending" — for example, right after a
create request, before the corresponding write has completed.
Calling getFile() on such a handle must reject with a
"NotAllowedError" DOMException, for the same reason a concurrent
requestFileHandle() call for the same hash does (see
§ 3.2 Reading files): even the caller that requested the handle must not be able to observe a
not-yet-verified placeholder as if it were the file’s real contents.
Once entry’s state is "written", getFile() called on
such a handle returns a File whose contents are entry’s bytes; this is exactly
the behavior [FS] already defines for a file entry whose binary data is
entry’s bytes.
5. Storage management
5.1. Storage limits
A user agent must impose a limit on the total number of bytes an origin may contribute to the
COS registry through successful writes, to prevent a single origin from
flooding the cache in an attempt to evict other origins' entries. The specific limit is
implementation-defined. If an origin’s write would exceed its limit, the user agent must
reject the write with a "QuotaExceededError" DOMException and should log a warning to
the console.
Note: Because entries are content-addressable, an origin that repeatedly writes the same bytes under the same hash does not consume additional quota beyond the first successful write; see § 2.2 COS entries.
A user agent also has an implementation-defined maximum origins list length, a
positive integer bounding how many origins a single origins list may
contain. This is enforced both when a list is first supplied (see validate a COS request)
and when one is later merged into an existing entry (see
upgrade resource visibility), so that neither a single call nor
the cumulative effect of many calls over time can grow an entry’s origins without
bound. Beyond bounding memory use, this also keeps a list of origins from being usable as an
undeclared substitute for "*"; see § 7.2 Cross-site probing.
Exceeding this limit is handled differently in each of those two places, because the two calls happen at very different points in an operation:
-
In validate a COS request, the limit is checked before any file is fetched, hashed, or written. A caller that exceeds it gets an immediate "
TypeError" and nothing else happens —the same treatment as any other malformedoriginsvalue. -
In upgrade resource visibility, the limit can only be reached by the cumulative effect of origins requested across separate, independent write calls, possibly by unrelated origins over a long period of time, and it is only checked after this call’s bytes have already been hashed, verified, and durably stored. Rejecting the write at that point would discard a successful, already-verified — and potentially very large — write over an unrelated bookkeeping limit, and would not even be attributable to a single caller’s mistake. The write is therefore left to succeed, and only the origins beyond capacity are dropped from origins, with a console warning. This mirrors the existing handling of a permissive-to-more-restricted request: an origin request that cannot be fully honored is silently capped rather than turned into a failure of the write that carried it. Consistent with § 7.3 Availability gating elsewhere in this specification, a caller has no reliable way to confirm after the fact whether its requested origin was actually added: a subsequent
requestFileHandle()call as that origin can still fail even if the origin was added — for example because of GREASE’ing — so its result must not be read as confirmation either way.
5.2. Eviction
This section is non-normative.
Under storage pressure, a user agent may evict entries from the COS registry, for example using a least-recently-used policy across the storing origins that have most recently accessed each entry. A user agent is expected to provide settings UI through which a user can inspect which files are stored, which origins have accessed each file, and manually delete entries or clear all Cross-Origin Storage data.
When a user clears an origin’s site data, the user agent should remove that origin from every storing origins set it appears in. If, after that removal, a COS entry’s storing origins is empty, the user agent may consider that entry for deletion.
5.3. Manually added entries
This section is non-normative.
The settings UI described above may let a user directly add a file they already have on disk to
Cross-Origin Storage — for example, an AI model they downloaded independently of any website —without any script ever calling requestFileHandle(). Such an entry
has no requesting origin to attribute the write to, which raises two questions the imperative
API does not answer by itself:
-
Who is the original storer, for the purposes of § 3.4.1 Original storer access? Nobody: the entry’s storing origins starts out empty, rather than containing an origin that performed the write. No origin is therefore exempt from the ordinary availability gating and origins checks that every requesting origin is otherwise subject to — which is the correct outcome, since no origin actually wrote the bytes.
-
Should origins default to same-site-only, as it does for an ordinary write that omits
origins? No: "same-site" is only meaningful relative to a requesting origin, and a manual add has none, so there is no site for "same-site" to mean. A user agent is expected to default such an entry’s origins to "*" instead, since manually seeding a file only serves its purpose — letting sites the user has not visited yet reuse a file the user already has on disk — if other origins can actually discover it. A user agent’s settings UI may additionally let the user choose a narrower scope.
Once added this way, an entry with state "written" is otherwise indistinguishable
from one a website wrote via requestFileHandle(): the same
availability gating and visibility
upgrade rules apply to it, exactly as they would for any other entry with the same
origins value — including the on the PHL check, if the entry ends up "*"-scoped,
which is the default for a manual add but not the only possible outcome.
6. Permissions Policy integration
This specification defines a policy-controlled feature [permissions-policy] identified by
the string "cross-origin-storage". Its default allowlist
is self.
A Document that is not allowed to use this feature causes every
requestFileHandle() call made from that Document (or from a
worker whose owner is that Document) to reject with a "NotAllowedError" DOMException,
before any hash validation, registry lookup, or write occurs.
7. Privacy and security considerations
This section is non-normative except where it uses RFC 2119 keywords; see also the security and privacy questionnaire in the repository.
7.1. Resource integrity
Every COS entry is keyed by, and its bytes are verified against, a cryptographic hash (see
§ 3.3 Creating and writing files). A site can therefore be sure that a file obtained through
requestFileHandle() has exactly the same bytes it would have gotten
by fetching hash itself. Developers cannot enumerate the contents of Cross-Origin Storage or
access a file without already knowing its hash.
A COS hash is not a secret. It is a content identifier, trivially computable by anyone who already has the file, and is routinely public knowledge for the kind of widely distributed resources this specification targets — published, for example, alongside a model’s or a library’s release. Knowing a hash is therefore sufficient to look an entry up, but unlike a capability URL or a bearer token, it grants no access by itself: whether a lookup succeeds is governed entirely by origins and § 7.3 Availability gating, never by whether the hash happened to stay unpublished. Developers must not treat an unpublished hash as an access-control mechanism; an attacker interested in a specific file does not need to guess its hash, only to obtain the file itself (see § 7.2 Cross-site probing).
7.2. Cross-site probing
If a resource is only used by a narrow set of sites, an attacker able to learn that the resource is
present in Cross-Origin Storage can infer that the user likely visited one of those sites. Because
requestFileHandle() is the mechanism for learning presence, each call
is, in effect, a probe. Two independent mechanisms in this specification bound what a probe can
learn:
-
origins (see § 3.4 Resource visibility upgrades) lets a storing origin restrict disclosure to a chosen set of origins, so a proprietary or low-popularity resource need not be globally probeable at all.
-
§ 7.3 Availability gating additionally withholds disclosure of a "
*"-scoped resource from requesting origins outside its storing origins unless the hash is on the PHL, so that a resource stored withorigins: "*" is not automatically probeable by every origin on the web. This gate applies only to "*"-scoped entries; for list- and same-site-scoped entries, the first bullet’s restriction is the sole gate, since the storing origin has already bounded disclosure explicitly for those.
The first bullet only holds if a "chosen set of origins" stays meaningfully smaller than the web.
Nothing about the shape of origins stops a caller
from enumerating a very large number of origins — for example, a list assembled from a public
top-sites ranking — which would functionally approximate global disclosure while bypassing the
deliberate, explicit opt-in that "*" alone requires. The maximum origins list length (see
§ 5.1 Storage limits) bounds this: a limit small enough to fit genuine multi-property use cases
(a handful of related origins under common control) but far short of any meaningful approximation
of "every origin" keeps the restricted-origins form of origins from being usable as
an undeclared substitute for "*".
A user agent should additionally rate-limit or otherwise throttle repeated
requestFileHandle() calls from a single origin, and may apply
on-device heuristics (for example, detecting a pattern of requests for hashes that appear to be
generated per-user) to identify and block probing attempts, independent of whether the probed
hashes are on the PHL.
7.3. Availability gating
Whether a requestFileHandle() read from an origin outside
storing origins succeeds always depends on whether the requesting origin is
allowed to read the entry, governed by origins. For a "*"-scoped entry, a second,
independent question also applies: whether the user agent is willing to disclose that the entry
exists at all, governed by PHL membership and GREASE’ing. Both must
hold for such an entry; for a list- or same-site-scoped entry, only the first question applies,
though GREASE’ing may still suppress a disclosure that origins would otherwise
permit. See § 3.2 Reading files for the normative algorithm. A "NotFoundError" is therefore not
proof that a resource is absent: it may mean the resource does not exist, that the requesting
origin is out of scope, that a "*"-scoped resource’s hash is not (yet, or ever) on the PHL, or
that GREASE’ing suppressed a true positive. Developers must not treat "NotFoundError" as
anything more specific than "fall back to the network."
7.4. GREASE’ing
A user agent may apply GREASE’ing (Generate Random Extensions And Sustain Extensibility): occasionally responding as if a disclosable entry were absent, even though § 7.3 Availability gating would otherwise permit disclosure. This adds noise that makes it harder for a site to distinguish a true absence from a privacy-motivated false negative, similar to the technique applied by UA Client Hints.
A user agent applying GREASE’ing must do so with judgement proportionate to the size of the entry in question. For small entries, where falling back to a network fetch is inexpensive, an occasional false negative is a reasonable privacy trade-off. A user agent must not apply GREASE’ing to entries whose size makes a spurious re-download clearly disproportionate to the resulting privacy benefit —for example, gigabyte-scale AI model weights — because a false negative there forces a full, observable, and costly re-download rather than a cheap one.
7.5. Fingerprinting
The information an attacker can extract from probing Cross-Origin Storage is bounded by how popular the probed resource is. Learning that a user has a very popular resource, such as a common AI model or a widely used JavaScript library, reveals only that the user visited one of the (possibly very many) sites that use it. Learning that a user has a rare or unique resource is far more informative. A user agent is expected to apply on-device heuristics to detect resources whose hashes look deliberately unique per user (for example, a hash that has only ever been written by a single site, or that is requested with unusual frequency) and to treat such patterns as probing attempts, independent of nominal PHL status.
8. Integration with other specifications
This section is non-normative.
The COS registry defined here is also reachable declaratively, without going through
requestFileHandle() directly, from three host languages. Each
integration is defined in the specification for its own host language, not in this document; this
specification defines only the shared underlying concepts (COS hash, COS entry,
availability gating, and the Public Hash List) that those
integrations are expected to build on.
-
HTML: a
crossoriginstorageattribute onlinkandscriptelements that already carryintegrity[SRI], to be proposed to the WHATWG HTML Standard. -
JavaScript: a host-defined
crossOriginStorageimport attribute, usable alongsideintegrity, building on the Import Attributes proposal, to be proposed to the WHATWG HTML Standard. -
CSS: a
cross-origin-storage()<request-url-modifier>, usable alongsideintegrity(), proposed to the CSS Working Group in w3c/csswg-drafts#14056.
For illustration only, here is the same globally-shared resource opted into Cross-Origin Storage through each of the three forms:
< script src = "popular-library.js" integrity = "sha256-abc123..." crossoriginstorage = "*" ></ script >
import datafrom "popular-resource.ext" with { integrity: "sha256-abc123..." , crossOriginStorage: "*" , };
@font-face { font-family : "Popular Font" ; src : url ( "popular-font.woff2" integrity("sha256-abc123..." ) cross-origin-storage ( *)); }
These snippets are illustrative only. None of this syntax is defined by this specification, and
the authoritative grammar for each form — including how a restricted, non-"*" origins value is
spelled — belongs to its own host-language specification, not to this document.
All three forms are expected to share the processing model of first consulting the COS registry for a matching, disclosable entry before falling back to a network fetch, and of
storing a successfully fetched and integrity-verified resource into the COS registry for reuse
by other origins, exactly as requestFileHandle() does. Discussion of
these integrations belongs in the tracker of the host-language specification that would carry them,
not in this repository’s issue tracker.
Acknowledgments
Many thanks for valuable feedback from Tab Atkins-Bittner, Yash Raj Bharti, and Joshua Lochner, and for valuable inspiration or ideas from Kenji Baheux and Kevin Moore.
This specification includes material modeled on File System, which is available under the W3C Software and Document License.