Conversation
…ookies The explainer said user agents were expected to make this API available only where third-party cookies are enabled. That reads as the feature switching off the moment a browser improves its privacy posture, which is what Anne raised in WebKit's standards position. Replace it with the mechanism that actually bounds the risk. Each distinct hash an origin can resolve cross-origin yields at most one bit, so an identifier's width is exactly the number of distinct cross-origin probes granted. Add a "maximum cross-origin probe count", a per-origin budget and probe set, and a "charge a cross-origin probe" step on the read path. Counted in distinct hashes rather than calls, charged whether or not the file is found, unpartitioned by top-level site, replenished on user activation, and cleared with site data. Storing origins and pending writers are exempt, so a site pays once per resource and never for one it wrote or is writing. Also separate the two threat models throughout. The Public Hash List and GREASE'ing bound inference about a user's browsing, but neither bounds a tracker that writes the resources itself: a k-anonymity bar constrains an attacker that can only observe state, not one that can set it.
|
@johannhof @arturjanc requesting your review here. GitHub only lets review requests go to repo collaborators, hence the mention instead. This is the change that came out of the discussion around Anne's comment: the third-party cookie condition is gone, replaced by a per-origin cross-origin probe budget that bounds identifier width directly. |
Per review: expressing this as "each origin has an associated budget that persists across page loads" hand-waves the persistence. Make it a global cross-origin probe map from origins to records, matching how the COS registry is already specified, with budget and probed hashes as struct items and a "get the cross-origin probe record" helper.
Per review: subdomains are free, so a per-origin budget is defeated by minting 01.example.com, 02.example.com and so on, one per bit, each collecting a full budget. A registrable domain has a cost, which is what makes the key meaningful. The cross-origin probe map is now keyed by site, obtained from the requesting origin, and the explainer and questionnaire follow. This does not address the second half of that review comment, whether the limit should additionally be enforced per top-level site. That is still open.
Per review: the budget reset was written as "when a user clears an origin's site data", but a site can clear its own via Clear-Site-Data. That gave any site an unbounded probe count for the price of one header, defeating the mechanism entirely. Only a user-initiated clear, through the user agent's own settings or storage UI, discards a cross-origin probe record. A site-initiated clearing must not modify the map at all. The rule also moves from Eviction, which is non-normative, up to Storage limits alongside the map definition, so a security-critical requirement carries normative force.
Per review: every sandboxed iframe gets a fresh opaque origin, so a page could mint unlimited probe budget by spawning them and postMessage-ing the bits back. The spec previously left the opaque calling-origin case undefined, which is no longer tenable now that a budget hangs off the caller's identity. Reject with NotAllowedError. An opaque origin can never be a storing origin, never matches a same-site comparison, and offers no key to bound a budget under, so there is nothing coherent to grant it.
Per review: 32 is the more common size and still comfortably covers most trackers' user bases. Also note that many trackers would settle for 16 bits and absorb the collisions, which makes the point sharper than framing 33 as the threshold to worry about.
Proposal for the three open threads on budget keying@johannhof @tomvangoethem, three of the open threads are the same defect seen from different angles, so answering them separately would fork one discussion into three. Putting a concrete proposal here instead. First, a correction to something I said in two of those threads. I claimed a What is actually unbounded is assembly. N iframes, each at its own site, each drawing a full budget, What grows without limit is tenure. The budget replenishes on user activation with no ceiling on total distinct hashes, and a hash stays in the probe set free forever once resolved. So a site the user visits often keeps everything it has already resolved and keeps earning more. Tom is right, and the current text is more permissive than his reading of it: it is a lifetime accumulator, not a per-visit allowance. Proposed shape, two changes:
What I am unsure about. Whether a shared page budget lets a hostile embed starve a legitimate one, and whether the eventual constant is small enough to matter given that concurrent width would still be on the order of the cap plus one visit's budget rather than exactly the cap. Does this look like the right shape? Happy to take it to a meeting instead if that is faster. |
The 2000-burst / 20-per-second token bucket was a throughput limit, and it was being read as this proposal's privacy defense, which it was never derived to be. It was quoted back in #72 as evidence against the design, fairly, since 2000 binary queries is ample to identify a user. Remove the figures and separate the two concepts the section conflated: rate limiting as ordinary abuse resistance, and a probe budget bounding total distinct hashes as the actual privacy mechanism. Point at #72 and #73, where the replacement is being worked out, rather than publishing a number nobody should implement yet.
The 2000-burst / 20-per-second token bucket was a throughput limit, and it was being read as this proposal's privacy defense, which it was never derived to be. It was quoted back in #72 as evidence against the design, fairly, since 2000 binary queries is ample to identify a user. Remove the figures and separate the two concepts the section conflated: rate limiting as ordinary abuse resistance, and a probe budget bounding total distinct hashes as the actual privacy mechanism. Point at #72 and #73, where the replacement is being worked out, rather than publishing a number nobody should implement yet.
The cross-site probing section treats a requestFileHandle() call as the unit of probing, which was already incomplete for the three declarative integrations and is worse now that fetch() can reach COS: a site learns the outcome of any integration lookup by observing whether its own server received the fallback request, which is the same single bit a NotFoundError carries. No integration discloses more than the imperative API, and origins scoping, availability gating, and GREASE'ing apply to all of them unchanged. The limit is what breaks: counting only imperative calls leaves it trivially avoidable, since a fetch() lookup is as scriptable in a loop as requestFileHandle() is. Say so in the explainer, in the spec's non-normative note, and in question 3 of the questionnaire. This bears on the probe budget being worked out in #72 and #73, which currently counts imperative calls only.
Brings in the fetch() host integration and the link fixes. Three conflicts, all where main's "integration lookups are probes too" change met this branch's probe budget: - README and index.bs: main said a probe limit has to count all four surfaces; this branch has two bounds rather than one, so say that a hash first resolved through the fetch() integration is rate-limited and charged against the budget exactly as an imperative read is, and move the paragraph up into the cross-site probing section where the bounds are introduced. - Questionnaire Q02 and Q03: keep this branch's rewrites and fold in main's additions, the COS-entry-is-bytes-only note in Q02 and the integration probing paragraph in Q03, phrased against the budget.
The three notes added in 591820e say an integration lookup counts as a probe, but nothing made that true. The budget is charged in "complete a read request", which only requestFileHandle() invokes, so a fetch() with crossOriginStorage reached the registry without paying anything. Section 8 likewise listed what integrations build on and omitted the budget. Factor the read path into an exported "resolve a COS lookup", taking a hash and an origin and returning an entry or one of "refused", "pending", "unavailable". It carries everything that bounds a read: the opaque-origin refusal, the storing-origins and pending-writers exemptions, the probe charge, and availability gating. "complete a read request" now maps the failure values onto the DOMException each case calls for and wraps a success in a handle; integrations run the same algorithm and must treat all three failures alike as a cache miss. This also closes a smaller leak: the opaque-origin refusal lived in the requestFileHandle() steps alone, so an integration invoked from a sandboxed iframe would have sidestepped it and minted fresh budget. Explainer and questionnaire follow: the budget's shape list now says it is charged on every surface, and "not calls" becomes "not lookups".
Refs #72.
In response to Anne's comment on the WebKit standards position, WebKit/standards-positions#703, and to the probe-limit critique in #72.
The explainer expected this API to be available only where third-party cookies are enabled, which reads as the feature switching off as soon as a browser improves its privacy posture.
What this changes
The third-party cookie condition is gone. In its place is a cross-origin probe budget. Each distinct hash a site can resolve cross-origin yields at most one bit, so the width of any identifier built by writing a chosen subset of resources and reading back which ones disclose is exactly the number of probes granted.
The budget is:
01.example.com,02.example.comand so on, one per bit.Clear-Site-Data, must not touch it, or a site could clear itself between probes and draw an unbounded number of them.Storing origins and origins with a write in flight are exempt, so a site pays once per resource and never for one it wrote or is writing.
requestFileHandle()now also rejects when the calling origin is opaque, since every<iframe sandbox>gets a fresh one and could otherwise mint unlimited budget.The explainer, spec and questionnaire also now separate two threat models that the earlier text ran together. The Public Hash List and GREASE'ing bound what an attacker can infer about a user's browsing. Neither bounds a tracker that writes the resources itself, since a k-anonymity bar constrains an attacker that can only observe state and not one that can set it.
What is deliberately not settled here
This PR establishes the mechanism, not its final shape. Review raised several refinements that are being tracked separately rather than growing this PR:
Thanks to @johannhof and @tomvangoethem for the review that produced most of the above.
Tests: web-platform-tests/wpt#61811.
index.htmlis left for CI to regenerate.