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 § 8 Privacy and security considerations.
The entry point for this specification is the getFileHandle()
method, exposed via crossOriginStorage:
const hash= { algorithm: 'SHA-256' , value: '8f434346648f6b96df89dda901c5176b10a6d83961dd3c1ac88b59b2dc327aa4' , }; try { const handle= await navigator. crossOriginStorage. getFileHandle( 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 file system that is
shared across origins and separate from any origin’s private or user-visible one.
This specification defines the imperative JavaScript API, plus the shared store step that host
integrations build on. Related proposals integrate the same underlying cache into other host
specifications: an HTML crossoriginstorage attribute on link and script, a
crossOriginStorage JavaScript import attribute, a CSS cross-origin-storage()
<request-url-modifier>, and a crossOriginStorage RequestInit option for fetch(). Each is
defined in its own host specification; see § 9 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, even though its value space is exactly the set of names accepted by [WEBCRYPTO]’s
hash algorithms. A HashAlgorithmIdentifier is (object or DOMString), and its object branch,
normalized by [WEBCRYPTO] into an Algorithm-shaped dictionary, lets callers 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, 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: value is already normatively lowercase (see above), so comparing it is a plain string comparison. algorithm has no normative case, which is why it is compared ASCII case-insensitively.
A content-addressable store is one whose entries are keyed by the equality of a COS hash: 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
-
A byte sequence whose digest under hash’s algorithm equals hash’s value.
Note: An entry is added to the COS registry only once a writer has supplied these bytes and the user agent has verified them; see verify and store. A write that has been requested but not completed has no representation in the registry at all, which is what makes an in-progress write indistinguishable from an absent one on the read path. See § 8.3 In-progress writes.
- origins
-
A list of origins granted explicit, cross-origin read access, independent of the Public Hash List. The grant extends to every origin same site with an item of the list, so listing
https://a.examplealso admitshttps://www.a.example. Initially empty, it only ever grows. This is one of three independent, additive grants that decide who may read an entry, the other two being its storing origins (always readable, together with origins same site with them) and globally disclosable.Note: A write whose
originsis an explicit empty list (origins: []) grants exactly what a write that omitsoriginsgrants: the entry’s storing origins and the origins same site with them, the baseline every entry carries (see determine COS disclosure). Applying the ceiling turns the empty list into null, so both writes reach the rest of complete a create request in the same state. An explicit list only ever adds recipients; it never withholds the entry from the writer’s own site. - globally disclosable
-
A boolean, initially false, recording whether the entry has been written with a request for global ("
*") availability. When true, the entry is readable by any origin whose request clears § 8.4 Availability gating, that is, whose hash is on the PHL and which GREASE’ing does not suppress. Once set, it stays true.Note: This is a separate item from origins, so that a later global-availability request adds the "
*" grant on top of an existing explicit origins list. Were the two conflated into one field, a writer that supplied the bytes could turn a list-scoped entry into a "*"-scoped one. Because § 8.4 Availability gating applies only to the "*" grant, an origin that could previously read the entry off the list would then start receiving a "NotFoundError" whenever the hash is not on the PHL. Keeping the grants independent makes every visibility change strictly additive, which is what upgradeable but never downgradeable requires. - 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
getFileHandle(), independent of origins or whether the entry’s hash is on the Public Hash List (PHL). See § 3.5.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 § 8.2 Cross-site probing). This is a risk specifically for the global grant. An entry’s explicit origins list, and the same-site baseline every entry carries, already have their disclosure bounded by an explicit choice of the storing origin, while making an entry globally disclosable potentially exposes it to any origin on the web. To bound that specific risk, a globally disclosable resource is only disclosable, through that grant, 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 the public suffix list, a companion data file to the [URL] Standard; 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. This is a concrete instance of the k-anonymity-style bar described above, applied once, offline, as part of how a hash is admitted, so the user agent never repeats it per query. None of this is yet established outside this proposal. An early, non-normative code prototype of the list itself lives in this repository for now, at public-hash-list/implementation/; a dedicated, cross-vendor repository remains the governance target described in the PHL explainer. This specification depends only on whether a hash is on the PHL, so it remains correct however the list’s retrieval mechanism, governance model, or the rest of that design evolves.
3. The CrossOriginStorageManager interface
[Exposed =(Window ,Worker ),SecureContext ]interface {CrossOriginStorageManager Promise <FileSystemFileHandle >getFileHandle (CrossOriginStorageGetFileHandleHash ,hash optional CrossOriginStorageGetFileHandleOptions = {}); };options dictionary {CrossOriginStorageGetFileHandleHash required DOMString ;value required DOMString ; };algorithm dictionary {CrossOriginStorageGetFileHandleOptions 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.
Note: Taking the origin from the relevant settings object, independent of anything about the
script itself, has a consequence worth spelling out for workers. A worker created from a
blob: URL has the origin of the context that created it, so it shares that origin’s
Cross-Origin Storage view exactly: what such a worker writes, its creating page can read back, and
what the page stored, the worker can read. The blob: URL is not an origin of its own, and
revoking it does not detach the worker from that view. A worker created from a data: URL
has an opaque origin. This specification does not currently define what
getFileHandle() does when the calling origin is opaque;
the opaque origin rule in validate a COS request governs only the origins a
caller names. An opaque origin has no stable identity to key
storing origins or same site comparisons on, so there is nothing meaningful for
it to be granted.
3.1. The getFileHandle() method
- handle = await navigator . crossOriginStorage .
getFileHandle(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 § 8.4 Availability gating. Callers can treat it as "fetch this from the network instead." - handle = await navigator . crossOriginStorage .
getFileHandle(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 .
getFileHandle(hash, {create: true,origins: "*" }) -
As above, additionally making the entry disclosable to any origin whose request for hash clears § 8.4 Availability gating, once written.
- handle = await navigator . crossOriginStorage .
getFileHandle(hash, {create: true,origins: ["https://a.example", "https://b.example"] }) -
As above, restricting disclosure to the listed origins and the origins same site with each of them (in addition to the calling origin, any origin already in storing origins, and the origins same site with those).
getFileHandle(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 the result of running check Cross-Origin Storage permission given global is false, 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.
CrossOriginStorageGetFileHandleHash hash
and a CrossOriginStorageGetFileHandleOptions 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.
Note: A write that another writer has requested but not yet completed is not in the COS registry, so entry is null for such a hash and this algorithm proceeds exactly as it does for a hash no origin has ever written. Every disclosure this specification permits is decided by apply availability gating below; there is no earlier branch that could answer before it. See § 8.3 In-progress writes.
-
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’s hash within the Cross-Origin Storage file system, in realm. -
Set handle’s COS origin to origin.
-
Set handle’s may read to true.
Note: This handle has already cleared apply availability gating, so the calling origin is entitled to the entry’s bytes.
-
Queue a global task on the DOM manipulation task source given global to resolve result with handle.
-
If entry is null, return null.
Note: An entry in the COS registry has always been written and verified, so there is no half-written state for this algorithm to consider. A hash whose only write is still in flight is absent from the registry and reaches this algorithm as null, the same as a hash no origin has ever written; see § 8.3 In-progress writes.
-
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.5.1 Original storer access.
-
If origin is same site with some item of entry’s storing origins, return entry.
Note: The same-site baseline every entry carries (the grant a write with no
originsoption requests) always holds, regardless of any origins list or globally disclosable grant added later. Because it is checked before the Public Hash List gate below, a same-site origin reads the entry whether or not its hash is on the PHL. Same-site here is one trust unit, so this discloses nothing across a site boundary that § 8.2 Cross-site probing guards. -
If origin is same site with some item of entry’s origins, return entry.
Note: An origin on the explicit origins list, or same site with one, reads the entry without the entry needing to be on the PHL. Extending each listed origin to its site mirrors the same-site baseline above: same-site origins are one trust unit, so naming one origin of a site names all of them. The storing origin made a bounded disclosure decision by naming it (see § 3.5 Resource visibility upgrades and § 7 The Cross-Origin-Storage-Allow-Origin header), and requiring separate global ubiquity on top of that would make ordinary restricted sharing depend on unrelated, public curation of what is often a proprietary resource. Checking this before the globally disclosable gate is what keeps a later global-availability write from revoking a listed origin’s access. See § 8.2 Cross-site probing.
-
If entry’s globally disclosable is true:
-
If entry’s hash is not on the PHL, return null.
Note: The Public Hash List gate applies only to the global grant, the one case where disclosure could otherwise reach any origin on the web. An entry that is globally disclosable but whose hash is not on the PHL is not disclosed to an origin that qualifies through this grant alone; an origin that also appears among storing origins, the explicit origins list, or the same-site set of either has already returned above.
-
If the user agent elects to apply GREASE’ing to this request, return null.
Note: GREASE’ing also applies only to the global grant. The grants checked above never reach this step: storing origins and their same-site origins are one trust unit, and an origin on the explicit origins list was named on purpose by a writer whose
Cross-Origin-Storage-Allow-Originheader authorized it, which covers its same-site origins as well. -
Return entry.
-
-
Return null.
-
Return the result of running determine COS disclosure given entry and origin.
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.
Every FileSystemFileHandle addressing the Cross-Origin Storage file system also has an
associated COS hash, the COS hash it addresses, an
associated
COS origin, the origin that obtained it, and an
associated
may read boolean, which governs whether
getFile() may disclose the addressed entry’s bytes through that particular
handle. It is true for a handle returned by complete a read request, which has already passed
availability gating, and false for a handle returned by
complete a create request until verify and store succeeds for that same handle.
Note: A handle addresses a COS hash, not a COS entry. complete a create request returns one for a hash that has no entry in the COS registry and might never acquire one, so there is no entry for it to reference until verify and store creates one.
Note: This is deliberately a property of each handle. A create request
returns a handle whether or not an entry for the hash already exists (see
complete a create request), so a per-entry rule would let any origin call
getFileHandle() with
create: true and immediately read an entry it
never wrote, learning its contents without satisfying origins,
PHL membership, or GREASE’ing. Every disclosure control in this
specification is applied on the read path, so gating per entry would let a create request walk
around all of them. Requiring the caller to supply the correct bytes first means a successful
read through a create-obtained handle discloses nothing the caller did not already have.
CrossOriginStorageGetFileHandleOptions options, a global object
global, and a Realm realm:
-
Let requestedOrigins be the result of running normalize requested origins given options["
origins"]. -
If requestedOrigins is a list, set requestedOrigins to the result of running apply a COS scope ceiling given requestedOrigins and the result of running obtain the COS scope ceiling given global.
-
Let handle be the result of creating a new
FileSystemFileHandle[FS] whose locator addresses hash within the Cross-Origin Storage file system, in realm. -
Set handle’s COS hash to hash.
-
Set handle’s COS origin to global’s relevant settings object’s origin.
-
Set handle’s requested origins to requestedOrigins.
-
Set handle’s may read to false.
Note: This algorithm does not consult the COS registry and does not modify it. It cannot: its result has to be independent of whether hash is already stored, or a create request would itself disclose prior presence (see § 8.2 Cross-site probing). handle is therefore returned on exactly the same terms whether the entry exists, was never written, or is being written by someone else right now, and it carries no read access until this caller supplies the bytes.
Note: Because nothing is registered here, a caller that requests a handle and then abandons it, never calling
createWritable()or never closing the resulting stream, leaves the registry exactly as it found it. No other origin can observe that the request happened, and no hash can be pinned in a placeholder state that suppresses later reads of it. See § 8.3 In-progress writes.Note: requestedOrigins is applied only by verify and store, through upgrade resource visibility, once the bytes have been written and verified. This is what makes a create request supply the full bytes before it can widen visibility; see § 3.5 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, before any later merge 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 the Cross-Origin Storage file system is closed [FS], the user agent must run the
verify and store steps below, given that handle, 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.
FileSystemFileHandle handle whose stream was
closed, the complete written byte sequence bytes, and the closing operation’s
relevant settings object’s origin origin, are:
-
Let hash be handle’s COS hash.
-
Let computedValue be the lowercase hexadecimal digest of bytes, computed using the algorithm named by hash’s algorithm, per [WEBCRYPTO].
-
If computedValue is not exactly equal to hash’s value:
-
Reject the closing operation’s promise with a "
DataError"DOMException. -
Abort these steps.
Note: A failed write has nothing to clean up. complete a create request registered no entry, so a write that never produced verified bytes leaves the COS registry untouched, and a later
getFileHandle()call for the same hash gets the same "NotFoundError" it would have got had the write never been attempted. An entry some origin has already written is likewise unaffected: this algorithm only ever adds bytes and grants. -
-
Enqueue the following steps to the Cross-Origin Storage queue:
-
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 bytes, origins is an empty list, globally disclosable is false, and storing origins is an empty set.
-
Set the COS registry[hash] to entry.
Note: This is the only step in this specification that adds an entry to the COS registry, and it runs only after bytes has been verified against hash. Two writers that both find the hash absent and write it concurrently each arrive here; whichever is enqueued second finds the entry the first created and merely adds its own origin and grants below. Their bytes hash to the same value, so storing them once is sufficient.
-
-
Append origin to entry’s storing origins, if not already present.
-
Run upgrade resource visibility given entry and handle’s requested origins.
-
Set handle’s may read to true.
Note: Only this handle becomes readable. Another handle for the same hash, obtained from a different create request that has not been written through, stays unreadable.
-
createWritable() called on a FileSystemFileHandle addressing the
Cross-Origin Storage file system must produce a FileSystemWritableFileStream whose contents
start empty, regardless of the value of keepExistingData and
regardless of whether the addressed COS hash has an entry in the COS registry.
Note: [FS] otherwise defines keepExistingData as seeding the
stream with the existing file’s contents. Honoring that here would let a caller become a storing
origin for bytes it never possessed. A create request returns a handle whether or not the entry
already exists, so a caller could open a writable for a hash another origin stored, close it
having written nothing, and have the carried-over bytes hash to the requested value. That would
grant it read access with origins, the Public Hash List, and
GREASE’ing never consulted: an availability gating bypass
assembled out of two individually permitted operations. It is the same reasoning that makes
verify and store require the complete contents on every create request.
Note: A writable closed without any write therefore holds the empty byte sequence, which will not
hash to the requested value for any entry a caller would want, so verify and store rejects
with a "DataError" DOMException as it does for any other mismatch. An implementation that
instead reports a storage or I/O failure for that case hides a verification result behind an
unrelated error.
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. Storing fetched resources
The host integrations listed in § 9 Integration with other specifications store a resource they
fetched without going through a FileSystemFileHandle. Their host specifications are expected
to run the following algorithm once the fetched bytes have passed the integrity check that the
integration requires.
*", a list of strings,
or null origins, and an environment settings object settings:
-
If the result of running check Cross-Origin Storage permission given settings’s global object is false, return.
-
Let options be «[ "
origins" → origins ]» if origins is not null, or «[ ]» otherwise. -
If the result of running validate a COS request given hash and options is not null, return.
-
Let computedValue be the lowercase hexadecimal digest of bytes, computed using the algorithm named by hash’s algorithm, per [WEBCRYPTO].
-
If computedValue is not exactly equal to hash’s value, return.
Note: The integration’s own integrity metadata can list several digests, and a match against any one of them satisfies it. This step makes sure the entry is keyed by a hash that these exact bytes produce.
-
Let requestedOrigins be the result of running normalize requested origins given options["
origins"]. -
If requestedOrigins is a list:
-
Let internalResponse be response’s internal response if response is a filtered response, or response otherwise.
-
Set requestedOrigins to the result of running apply a COS scope ceiling given requestedOrigins and the result of running parse a COS scope ceiling given internalResponse’s header list.
Note: The ceiling comes from the fetched resource’s own response, so the server that supplies the bytes decides which origins they can be disclosed to. The loading
Document’s COS scope ceiling plays no part: its operator does not control the resource, and letting it authorize recipients would let any site declare another site’s asset shareable with origins of its choosing. The user agent reads the header from the internal response, so this works for an opaque filtered response as well, and the header is never exposed to the page. -
-
Let origin be settings’s origin.
-
Enqueue the following steps to the Cross-Origin Storage queue:
-
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 bytes, origins is an empty list, globally disclosable is false, and storing origins is an empty set.
-
Set the COS registry[hash] to entry.
Note: As in verify and store, the entry is created only once bytes has been verified against hash, which the integration’s
integritycheck has already done. AgetFileHandle()writer for the same hash that is still in flight is unaffected: it holds no registry state, and its own verify and store settles normally, finding this entry and adding its origin and grants to it. -
-
Append origin to entry’s storing origins, if not already present.
-
Run upgrade resource visibility given entry and requestedOrigins.
-
Note: As with verify and store, a store always requires the complete bytes, including when the
entry is already "written", so it reveals nothing about prior presence. The integration’s lookup
before its network fetch is a read, subject to availability gating and to
the probing considerations in § 8.2 Cross-site probing.
3.5. 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
originsrequests only same-site availability, which every entry already has (see determine COS disclosure), so there is nothing to add. -
If requestedOrigins is "
*":-
Set entry’s globally disclosable to true.
-
Return.
Note: This adds the global grant; it does not touch entry’s origins. An entry that already carried an explicit origins list keeps it, so every origin on that list keeps its PHL-independent access even after the entry becomes globally disclosable. This is the crux of why the two grants are stored separately; see globally disclosable.
-
-
For each candidateOrigin of requestedOrigins:
-
If entry’s origins’s size equals the user agent’s maximum origins list length, then break.
Note: Any remaining candidateOrigin values are silently dropped from this upgrade, since the write itself already succeeded, and the user agent is expected to log a console warning that the entry’s origins list is at capacity.
Note: An upgrade only ever adds to origins or sets globally disclosable
to true. A writer that requests a list widens an entry that is already
globally disclosable just as it widens one that is not, because the two grants are
independent: the newly listed origins gain PHL-independent access even while the
"*" grant remains in force.
Note: Any site, including one other than 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.5.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 getFileHandle(), 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.
queryPermission() and requestPermission() on such a handle must therefore never report
"prompt", since no prompt can appear. They must report "denied" for a write mode on a handle
that was not obtained by a create request, and "granted"
otherwise.
Note: Writing is the one capability a handle can genuinely lack. Only a create request yields a
handle that can be written through, so reporting "granted" for a write mode on any other handle
would claim a capability createWritable() will refuse. Reading stays
"granted", since a caller holding a readable handle can read through it. Requesting cannot change
either answer, as there is no prompt to show and no state a grant could be recorded in.
Calling getFile() on a FileSystemFileHandle addressing the
Cross-Origin Storage file system whose may read is false must reject
with a "NotAllowedError" DOMException. This covers two distinct cases:
-
A handle from a create request that has not been written through yet, whose addressed hash has no entry in the COS registry. There are no verified bytes to return, and the caller must not be able to observe its own unfinished write as if it were the file’s real contents.
-
A handle from a create request for a hash that some other origin has already written. Verified bytes exist, and this caller has not supplied them and has not passed availability gating, so it must learn nothing about the entry, not even whether it exists.
Note: The rejection is identical in both cases, so it discloses nothing. This is why
"NotAllowedError" is safe here although the read path deliberately avoids it: this handle is
the caller’s own, obtained by its own create request, and a handle cannot be deserialized in any
other origin (see § 4.1 Transferring handles). The answer therefore never crosses an origin boundary, and
it is constant with respect to the registry’s contents. Contrast § 8.3 In-progress writes, where a
distinct error on the read path would have crossed that boundary.
Once a handle’s may read is true, getFile()
called on it returns a File whose contents are the addressed entry’s bytes; this
is exactly the behavior [FS] already defines for a file entry whose
binary data is those bytes.
A FileSystemFileHandle addressing the Cross-Origin Storage file system has a
file system locator whose path is a list containing a single
string, its COS hash’s value, and whose
root is the Cross-Origin Storage file system.
Note: [FS] defines name as the last path component of the handle’s
locator’s path, so this also settles name: it is the
hash. An entry has no name of its own, and the hash is the only identity it has, so
reporting it carries information the empty string would discard.
An entry has no name and no containing directory, so most of [FS]’s structural surface has nothing to act on. Identity is the exception, and it is the one question a content-addressed entry can always answer: the COS registry holds at most one COS entry per COS hash, so two handles address the same entry exactly when their hashes are equal.
isSameEntry() called on a FileSystemFileHandle addressing a
the Cross-Origin Storage file system, with an argument that also addresses it and was obtained
by the same origin, must return true if the two handles'
COS hash items are equal, and false otherwise.
Note: Refusing this would discard information the user agent already holds.
isSameEntry() called with an argument the calling origin did not obtain,
or one addressing a file system other than the Cross-Origin Storage file system, must reject.
Note: Returning false in that case would assert that the two handles address different entries, which is a claim about a handle the calling origin is not entitled to inspect. Rejecting says only that the comparison could not be made.
move() and remove() called on a
FileSystemFileHandle addressing the Cross-Origin Storage file system must reject with a
"NotAllowedError" DOMException, and must leave any addressed entry unchanged.
Note: An entry is shared by every origin in its storing origins, so honoring a removal would let one site destroy data other sites depend on; deletion belongs to eviction and to the user’s own storage controls. A rename or a reparent has nothing to act on, since an entry is named by its COS hash and has no containing directory.
Note: Neither method is defined by [FS] or by the File System Access API at the time of writing
(WICG/file-system-access#214),
so the error name follows removeEntry(), the operation [FS] does
define for deleting an entry: it rejects with the access result’s error name when readwrite access
is not granted. Rejecting is the accurate behavior here: the operations are meaningful and
denied.
createSyncAccessHandle() needs no rule here: its steps reject with an
"InvalidStateError" DOMException whenever the handle is not in a bucket file system,
and the Cross-Origin Storage file system is a file system root distinct from any origin’s,
so [FS] already determines both the refusal and its name.
Note: That is also the outcome this specification would want independently. A
FileSystemSyncAccessHandle hands the caller a writable file descriptor, which would let it
change an entry’s bytes out from under the COS hash they are stored against,
and every other origin the entry is disclosable to reads those same bytes.
4.1. Transferring handles
A FileSystemFileHandle is a serializable object [FS], so one addressing a
COS entry can be passed to another environment settings object, for example with
postMessage(message, options). That is a second way to come by such a handle, alongside
getFileHandle(), so it is constrained here as well.
The serialization steps for a FileSystemFileHandle addressing the
Cross-Origin Storage file system, given value and serialized, additionally:
-
Set serialized.[[COSHash]] to value’s COS hash.
-
Set serialized.[[COSOrigin]] to value’s COS origin.
-
Set serialized.[[COSMayRead]] to value’s may read.
The deserialization steps, given serialized, value and realm, additionally:
-
If serialized.[[COSOrigin]] is not same origin with realm’s environment settings object’s origin, then throw a "
DataCloneError"DOMException. -
Set value’s COS hash to serialized.[[COSHash]].
-
Set value’s COS origin to serialized.[[COSOrigin]].
-
Set value’s may read to serialized.[[COSMayRead]].
Note: Without the same origin check, transferring would defeat every disclosure control in this specification at once. A handle whose may read is true has already cleared apply availability gating for the origin that requested it; posting it to another origin would hand that origin the entry’s bytes without origins, PHL membership or GREASE’ing ever being evaluated for it. This matches the File System Standard’s existing treatment of handles, which are likewise only useful to a same origin recipient.
Note: may read travels with the handle, so a create-request handle that has not been written through stays unreadable after a transfer, and a read-request handle stays readable without being re-gated. Re-gating on arrival would let a caller probe availability repeatedly by transferring the same handle.
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 § 8.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, and the write that carried it still succeeds. Consistent with § 8.4 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
getFileHandle()call as that origin can still fail even if the origin was added (for example, because the entry was evicted or the user agent throttled the request), 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 getFileHandle(). 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.5.1 Original storer access? Nobody: the entry’s storing origins starts out empty. No origin is therefore exempt from the ordinary availability gating and origins checks that every requesting origin is otherwise subject to. That is the correct outcome, since no origin actually wrote the bytes.
-
Should such an entry be restricted to same-site-only, as an ordinary write that omits
originsis? 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 make such an entry globally disclosable, leaving its explicit origins list empty, 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 is otherwise indistinguishable
from one a website wrote via getFileHandle(): the same
availability gating and visibility
upgrade rules apply to it, exactly as they would for any other entry with the same grants.
That includes the on the PHL check whenever the entry ends up
globally disclosable, the default for a manual add.
5.4. Provenance metadata
This section is non-normative.
A COS entry deliberately records nothing about where its bytes came from. Identity is the hash alone: the same bytes are the same entry regardless of how many origins stored them or how many URLs they were fetched from (see § 2.2 COS entries). A URL is a property of one writer’s fetch, so there is no non-arbitrary way to pick one for an entry several origins wrote; it is not verifiable the way the bytes are (see § 8.1 Resource integrity); and it carries considerably more about a user’s browsing than the coarse origin values in storing origins do, since a path or query string can name a specific document, account, or session. Recording it on the entry would therefore route a high-entropy signal around the disclosure limits that origins and § 8.4 Availability gating exist to impose.
The motivation for wanting it is nonetheless real: a user looking at a multi-gigabyte file in the
settings UI described in § 5.2 Eviction, and a developer debugging why a
getFileHandle() call missed, would both like to know where a file
came from. A user agent may serve that by keeping an implementation-private provenance record
alongside an entry (for example, the URL each of its storing origins fetched the
bytes from, and when), provided the record is treated as user agent state kept outside the
entry:
-
It is never exposed to script. Nothing in this specification discloses it, and nothing added later should: neither
getFileHandle()nor theFileSystemFileHandleit returns should reveal whether such a record exists, let alone its contents. In particular, a requesting origin must not be able to observe a record it did not itself produce. -
It is surfaced only through trusted, non-web surfaces: the user agent’s own settings and storage inspection UI, its developer tools, and (where the user agent has an extension platform) extension APIs gated behind an explicit, user-granted permission, on the same footing as other APIs that expose browsing history. See the browser extension integration points explainer in this repository for the extension surfaces being considered.
-
It is unverified, and is presented as such. A URL in it is a claim by the origin that wrote the bytes. Only the hash guarantees the content (see § 8.1 Resource integrity). Two origins may record different URLs for the same entry, and either may be inaccurate or deliberately misleading.
-
It shares the lifetime of the entry, and of the origin it is attributed to. It is discarded when the entry is evicted or deleted, and the portion attributable to an origin is discarded when that origin is removed from storing origins, including when the user clears that origin’s site data (see § 5.2 Eviction).
Because such a record is invisible to content, whether a user agent keeps one, and how much it keeps, is left entirely to implementations. Nothing in this specification depends on it, and its presence or absence is not detectable by a site.
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.
[permissions-policy] defines allowed to use over Documents, but
CrossOriginStorageManager is also exposed on WorkerGlobalScope, which has no
Document of its own. The check is therefore stated over the calling global object: a
worker global resolves to the Documents that own it, transitively, and every one of them
must permit the feature.
-
If global is a
Windowobject, then:-
Let document be global’s associated Document.
-
If document is null, return false.
-
If document is not allowed to use the "
cross-origin-storage" policy-controlled feature, return false. -
Return true.
-
-
If global is a
WorkerGlobalScopeobject, then:-
Let owners be global’s owner set.
-
If owners is empty, return false.
-
For each owner of owners:
-
If owner is a
Documentand owner is not allowed to use the "cross-origin-storage" policy-controlled feature, return false. -
If owner is a
WorkerGlobalScopeobject and the result of running check Cross-Origin Storage permission given owner is false, return false.
-
-
Return true.
-
-
Return false.
A global object for which this algorithm returns false causes every
getFileHandle() call made from it to reject with a
"NotAllowedError" DOMException, before any hash validation, registry lookup, or write
occurs.
Note: Resolving through the owner set is what keeps the feature from being
opt-out in name only. A worker created from a blob: URL runs in its creating page’s
origin and shares that page’s Cross-Origin Storage view exactly (see
§ 3 The CrossOriginStorageManager interface), so a check that consulted only a global’s own
Document would be satisfied vacuously in every worker, and moving a call into
new Worker(URL.createObjectURL(…)) would escape a cross-origin-storage=() policy
without changing anything else about it.
Note: A ServiceWorkerGlobalScope’s owner set is empty, so this
algorithm returns false there. That follows from what an owner set is: a
liveness relation, populated when a worker is created and consulted to decide whether that worker
is still actively needed. A service worker deliberately has no such relation to any Document:
its registration is persisted, the user agent starts the global in response to events, and it can
run with no service worker client at all. The client that called
register() is therefore knowable at registration time but is
routinely gone by the time the global runs, and one running service worker serves many clients
whose policies need not agree.
Note: Covering service workers needs a captured policy, and that work belongs to
[permissions-policy] and [service-workers]. The shape
this specification would favor is to derive a service worker’s policy from the
Permissions-Policy header on its own script response, the way its policy container
already carries that response’s CSP: it stays under the origin’s control, and it is
re-evaluated on every update check. Snapshotting the registering client’s policy would make the opt-out stale, since removing the feature
from a header would not take effect until the registration happened to update. Until such a
model exists, failing closed is the one answer that cannot be used to escape an opt-out.
Note: A SharedWorkerGlobalScope’s owner set can hold several
Documents with different policies, and it gains and loses owners over the worker’s lifetime.
Requiring every owner to permit the feature means one owner that disallows it disables
Cross-Origin Storage for the whole shared worker, and that the answer can change between two
getFileHandle() calls. The alternative, requiring only one
permissive owner, would let any document that allows the feature re-enable it on behalf of
documents that disallow it.
7. The Cross-Origin-Storage-Allow-Origin header
A COS entry whose origins list is non-empty is disclosable to origins the
writing origin does not control, yet that list is declared by script (through
origins) or by markup, either of which an attacker
who has achieved script or markup injection can set. To tie that declaration to the operator of
the origin whose bytes are being disclosed, in a form injected content cannot forge, this
specification defines a response header that bounds which origins such a declaration may name.
The Cross-Origin-Storage-Allow-Origin response header carries a
COS scope ceiling: a set of origins that a write may name in a non-same-site
origins list. Whoever supplies the bytes sends the header, so it is read from
the response behind the bytes being disclosed:
-
For a
getFileHandle()write, whose bytes may be generated in script and have no originating response of their own, it is the response used to create the calling global object’sDocument, or, for a worker, those of theDocuments that own it (see obtain the COS scope ceiling). -
For a host integration, it is the response of the fetched resource whose bytes are stored (see store a fetched resource in Cross-Origin Storage). This is the resource’s server, which need not be the server of the
Documentthat loads it.
A declared list is intersected with the ceiling (see apply a COS scope ceiling), so the ceiling can only ever narrow a declaration.
Note: The header is an authorization by the side that supplies the bytes. The origins it names,
the eventual readers, are not consulted and send nothing: a listed origin takes part only later,
by calling getFileHandle() for the COS hash like any other
reader. This runs in the opposite direction from Access-Control-Allow-Origin [FETCH], where the
server holding a resource opts in to a given origin reading it. Here the supplier of the bytes
bounds who can later read them from Cross-Origin Storage, and injected content on the writing
page cannot extend that bound, because it cannot set the supplier’s response headers.
Note: The header applies only to the list form. The same-site default names no cross-origin
recipients and so needs no authorization, and the "*" form is governed at the read path by
availability gating against the Public Hash List, which a
per-user secret can never clear, so it needs no write-time ceiling either. Requiring a header for
"*" would also break the zero-configuration global-sharing case without closing anything the
Public Hash List does not already close.
-
Let value be the result of get, decode, and split "
Cross-Origin-Storage-Allow-Origin" from headers. -
If value is null, return an empty set.
-
Let ceiling be an empty set.
-
For each token of value:
-
strip leading and trailing ASCII whitespace from token.
-
If token is the empty string, continue.
-
Let origin be the origin resulting from running the basic URL parser on token.
-
If origin is failure or is an opaque origin, return an empty set.
Note: This includes a token of "
*", which does not parse to an origin. ACross-Origin-Storage-Allow-Originheader can only enumerate a finite set of concrete origins; "*" is not a permitted value, because a ceiling of "any origin" would let injected content declare any recipient it liked and so defeat the header’s entire purpose. Unbounded disclosure is reached only through the "*" origins form, which is gated by the Public Hash List and never consults this header. A malformed or "*"-bearing header authorizes nothing, exactly as an absent one does; the failure is closed. -
Append origin to ceiling.
-
-
Return ceiling.
Note: get, decode, and split splits on U+002C (,), so origins in the header
value are comma-separated, matching Timing-Allow-Origin [RESOURCE-TIMING] and the general
convention for
list-valued HTTP response headers, even though the sibling crossoriginstorage HTML content
attribute is space-separated to match its host syntax. Both resolve to the same
origin set.
-
If global is a
Windowobject, then:-
Let document be global’s associated Document.
-
If document is null, return an empty set.
-
Return document’s COS scope ceiling.
-
-
If global is a
WorkerGlobalScopeobject, then:-
Let owners be global’s owner set.
-
Let ceiling be null.
-
For each owner of owners:
-
Let ownerCeiling be an empty set.
-
If owner is a
Document, set ownerCeiling to owner’s COS scope ceiling. -
Otherwise, if owner is a
WorkerGlobalScopeobject, set ownerCeiling to the result of running obtain the COS scope ceiling given owner. -
If ceiling is null, set ceiling to ownerCeiling; otherwise set ceiling to the intersection of ceiling and ownerCeiling.
-
-
Return ceiling.
-
-
Return an empty set.
-
Remove from requestedOrigins each origin that ceiling does not contain.
Note: A recipient missing from the COS scope ceiling is dropped and the write still succeeds, matching how an over-length upgrade drops its excess in upgrade resource visibility. The user agent is expected to log a console warning naming each dropped origin.
-
If requestedOrigins is empty, return null.
Note: A list every one of whose members was dropped leaves the write with no authorized cross-origin recipient, so it falls back to the same-site default. The bytes are still stored; only the unauthorized disclosure is withheld.
-
Return requestedOrigins.
Every Document has a COS scope ceiling, a set of origins,
set when the document is created to the result of running parse a COS scope ceiling given the
header list of the response used to create it.
Note: Taking the intersection across a worker’s owner set mirrors
check Cross-Origin Storage permission: an origin is in a worker’s ceiling only if every owning
Document, transitively, authorized it. Because a blob: worker shares its creating page’s
origin and Cross-Origin Storage view exactly (see
§ 3 The CrossOriginStorageManager interface), a ceiling that consulted only the worker itself
would be empty for every worker, and moving a list-scoped write into
new Worker(URL.createObjectURL(…)) would strip the recipients the page authorized;
intersecting the owners' ceilings preserves that authorization without letting any single
owner widen it.
Populating a Document’s COS scope ceiling from its response requires
a hook in [HTML]’s document creation steps, the same way [permissions-policy] and [CSP] are
populated at response processing time. Until that hook exists, this section describes the
required behavior directly; a future revision is expected to integrate it formally, alongside the
[FS] hook noted in § 3.3 Creating and writing files. Host integrations need no such hook: they
pass the fetched response to store a fetched resource in Cross-Origin Storage, which reads
the ceiling from it.
8. 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.
8.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
getFileHandle() 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, often published alongside a model’s or a library’s release. Knowing a hash is therefore sufficient to look an entry up, and it grants no access by itself: whether a lookup succeeds is governed entirely by origins and § 8.4 Availability gating. 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 § 8.2 Cross-site probing).
8.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
getFileHandle() 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.5 Resource visibility upgrades) lets a storing origin restrict disclosure to a chosen set of origins and the origins same site with them, so a proprietary or low-popularity resource need not be globally probeable at all.
-
§ 8.4 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
getFileHandle() 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.
getFileHandle() calls, since a
scriptable integration can issue them in a loop just as readily.
8.3. In-progress writes
A write that has been requested but not completed must be indistinguishable, to every origin, from a hash that was never written at all. This specification achieves that structurally: a COS entry is added to the COS registry only by verify and store or store a fetched resource in Cross-Origin Storage, in both cases only after the complete bytes have been verified against the hash. complete a create request neither reads nor writes the registry. There is consequently no placeholder state for a read to observe, and no branch on the read path that could answer before apply availability gating does.
The alternative, registering a placeholder when a create request is made and rejecting concurrent reads of it with a distinguishable error, would let a caller learn that some origin is writing a given hash right now. That single bit sounds minor and is not. It would be available:
-
to any origin, including one with no grant of any kind on the entry, since the placeholder carries no grants and the read would be answered before determine COS disclosure ran;
-
for any hash, including one that is not on the PHL and never could be, so an attacker could use hashes derived from a per-user value, which is exactly the pattern § 8.6 Fingerprinting exists to catch;
-
without GREASE’ing, which applies only to disclosures that reach the gating step, so the bit would be noiseless;
-
without supplying any bytes, so neither the per-origin storage limit nor § 5.2 Eviction would bound how many such bits an origin could create.
A tracker embedded on two sites could therefore mint an arbitrary-width identifier on the first and read it back on the second, at no storage cost, bypassing origins, the Public Hash List, and GREASE’ing together. A placeholder would also be a denial-of-service vector: pinning the hash of a genuinely popular resource would suppress every other origin’s reads of it for as long as the placeholder survived.
Note: What this costs is the ability for one caller to discover that another is already downloading
a large file, and so to avoid starting a redundant download of it. That is a real cost, and it is
the reason an earlier draft of this specification used a distinguishable error here. It is
recoverable where it matters most, within a single origin, using
Web Locks, BroadcastChannel, or a shared worker,
none of which disclose
anything across an origin boundary. Across origins the saving is largely theoretical: two unrelated
origins would have to race on the same hash at the same moment, and an entry that neither has
written yet is not disclosable to the other in any case until it is on the PHL.
Note: "NotAllowedError" remains the response for getFile() on a
create-obtained handle that has not been written through (see § 4 The Cross-Origin Storage file system). That answer is
constant with respect to the registry and reaches only the origin that made the request, since
handles cannot be deserialized cross-origin (see § 4.1 Transferring handles), so it is not a channel.
8.4. Availability gating
Whether a getFileHandle() 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.
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."
Note: In a user agent that supports third-party cookies, the "*" grant lets a tracker link a
user’s visits across sites no better than a third-party cookie already can. The PHL alone
does not bound this: an attacker can build a cross-site identifier out of individually
PHL-eligible resources by choosing which subset to write. A user agent that blocks third-party
cookies can still offer the "*" grant if it bounds cross-site disclosure, for example with a
small budget of cross-site lookups keyed on the top-level site and shared by every frame on
the page, a count that persists across reloads for a time window, and sharing of written entries only
after a user gesture. The README’s
privacy considerations describe these mitigations.
8.5. 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 § 8.4 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.
GREASE’ing applies only to an origin that qualifies through the globally disclosable grant alone, the same case the Public Hash List gates (see determine COS disclosure). A user agent must not apply it to storing origins, to origins on the explicit origins list, or to origins same site with either.
A user agent applying GREASE’ing must do so with judgment 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.
8.6. 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.
9. Integration with other specifications
This section is non-normative.
The COS registry defined here is also reachable from four host integrations without going
through getFileHandle() directly. Each
integration is defined in its own host specification. This specification defines the shared
underlying concepts (COS hash, COS entry, availability gating, and
the Public Hash List) and the store a fetched resource in Cross-Origin Storage step that
those integrations are expected to build on.
-
HTML: a
crossoriginstorageattribute onlinkandscriptelements that already carryintegrity[SRI], proposed to the WHATWG in whatwg/html#12770. -
JavaScript: a host-defined
crossOriginStorageimport attribute, usable alongsideintegrity, building on the Import Attributes proposal, proposed to the WHATWG in whatwg/html#12771. -
CSS: a
cross-origin-storage()<request-url-modifier>, usable alongsideintegrity(), proposed to the CSS Working Group in w3c/csswg-drafts#14056. -
Fetch: a
crossOriginStorageRequestInitoption, usable alongsideintegrity[FETCH], proposed to the WHATWG in whatwg/fetch#1954. This one has no host-defined destination, so how aResponseserved from the COS registry reports its MIME type, status, and other metadata is an open question for that specification; a COS entry stores bytes only.
For illustration only, here is the same globally-shared resource opted into Cross-Origin Storage through each of the four 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 ( *)); }
const response= await fetch( "popular-resource.ext" , { integrity: "sha256-abc123..." , crossOriginStorage: "*" , });
These snippets are illustrative only. None of this syntax is defined by this specification.
The authoritative grammar for each form, including how a restricted origins list is spelled,
belongs to its own host specification. The spellings deliberately
differ: a RequestInit member can take a sequence<DOMString>, matching
origins exactly, while an attribute value, an
import attribute value, and a CSS modifier argument can only carry text.
All four 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 getFileHandle() does. The store step
is store a fetched resource in Cross-Origin Storage, which each integration runs with the
fetched response. A restricted origins value is therefore bounded by the
Cross-Origin-Storage-Allow-Origin header on that resource’s response, sent by the server that
supplies the bytes (see § 7 The Cross-Origin-Storage-Allow-Origin header). Discussion of
these integrations belongs in the tracker of the host 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.