Correct three claims a second review pass found overstated

The mechanism paragraph said the server pair being refused is what lets the
widget's partitioned pair land. That is false: CHIPS makes the partition key
part of a cookie's identity, so the two are different cookies and coexist, and
#2218 measured all four at once on Chromium. Firefox is the exception because
Total Cookie Protection files the server cookie under the embedder's partition,
which makes the keys collide, and a script may not replace an HttpOnly cookie it
collides with. The section now reports the outcome per engine and stops
asserting a causal link the measurements do not show.

PartitionedCookies was called the better answer for either path with no limits
attached. Two apply. The option is global, so Service.Set puts it on the OAuth
callback too, and a cookie partitioned to the popup's own top-level context is
one the frame cannot see, which regresses OAuth on the permissive browsers
where it works today. And HttpOnly removes bearer-token theft, not authenticated
action during an XSS, since the XSRF value stays readable by design and script
on the widget origin can still make requests the browser attaches the cookie to.

The evidence claim was broader than any single run supports. The criterion asks
for sign-in, a post and a reload; the permanent suite does all three but only on
Chromium's default policy against a stack that is not the recommended recipe,
and the cross-browser campaign covered sign-in and reload on the recommended
recipe without posting. Both halves are now stated.

Also: 70 runnable top-level tests, not 71, which counted TestMain; and three
engines across four browser targets, since Safari is a WebKit browser.
This commit is contained in:
Dmitry Verkhoturov
2026-08-24 01:48:12 +01:00
parent ca7f55d659
commit d5ede91a9f
@@ -52,7 +52,14 @@ which is the same subject. Either the requirement excludes OAuth and #1139 is a
carrying its own timeline, or the requirement keeps OAuth and is not met today. This document
assumes the first and is not entitled to assume it.
The second is what counts as evidence. The criterion is met for anonymous and email by measurement,
The second is what counts as evidence, and the criterion is not met end to end by any single run.
The criterion asks for sign-in, a post and a reload. The permanent suite covers sign-in, post and
reload for anonymous and email, but on Chromium's default policy and against a stack running
`AUTH_SAME_SITE=none` alongside the header, which is not the recommended recipe; the blocking case
adds the control and the reload but does not post. The cross-browser campaign covered sign-in,
reload and the controls on the recommended recipe, and did not post. So persistence is measured and
posting-after-reload is not, on the same run. Taking those together, the criterion is met for
anonymous and email by measurement,
and for Telegram by inference from the client writer keying off `X-JWT` and not off the provider. A
standing requirement is meant to be a test a proposal passes, so either inference is acceptable
evidence here and the section should say so, or Telegram leaves the criterion partly pending until
@@ -116,18 +123,25 @@ it is partitioned. #2214's control cookie encodes exactly that: an unpartitioned
cookie written by the same script in the same frame has to be dropped, or the case declares itself
vacuous.
**Measured across four engines, and the mechanism is not uniform.** The above was reasoned from the
code and verified on Chromium. Driving the real thing on two genuinely different registrable domains
with real certificates, across Chromium, Firefox, WebKit and Safari 27, shows the recommended
arrangement working everywhere except one case, and working for two different reasons.
**Measured across three engines and four browser targets, and the mechanism is not uniform.** The
above was reasoned from the code and verified on Chromium. Driving the real thing on two genuinely
different registrable domains with real certificates, across Chromium, Firefox, WebKit and Safari
27, shows the recommended arrangement working everywhere except one case, and working for two
different reasons.
On Chromium, WebKit and Safari the server's own pair is refused, and that refusal is what clears
the field for the widget's partitioned pair. The reason differs by engine, which matters to anyone
reasoning forward from it. On Chromium the cause is the attribute: `AUTH_SAME_SITE` defaults to
emitting no `SameSite` at all, and Chromium treats a missing attribute as `Lax`, so the cookie is
not sent cross-site. On WebKit and Safari the cause is third-party blocking, which applies whatever
the attribute says. Only the refusal itself was measured on all three; the causes are read off
documented browser behaviour.
On Chromium, WebKit and Safari the server's own pair does not reach the frame, and the widget's
partitioned pair is what the session runs on. The reason the server pair is absent differs by
engine: on Chromium `AUTH_SAME_SITE` defaults to emitting no `SameSite` at all and Chromium treats
a missing attribute as `Lax`, so the cookie is not sent cross-site; on WebKit and Safari it is
third-party blocking, which applies whatever the attribute says. Only the outcome was measured; the
causes are read off documented browser behaviour.
The two pairs are not in competition, which an earlier reading of this had wrong. CHIPS makes the
partition key part of a cookie's identity, so a partitioned cookie and an unpartitioned one of the
same name are different cookies and coexist: #2218 measured all four at once on Chromium with both
settings on. Firefox is the exception precisely because Total Cookie Protection files the server's
cookie under the embedder's partition, which makes the keys match, and a script may not replace an
`HttpOnly` cookie whose key it collides with.
On Firefox the server's pair is accepted, and since the `JWT` it sets is `HttpOnly`, the browser
then forbids the widget's script from replacing a cookie of that name. So Firefox never holds a
@@ -479,8 +493,8 @@ widget *code* through npm breaks OAuth, but it adds a publish step and a version
### Task 2: Extend the e2e suite
**Done.** #2180 moved the suite to Go and playwright-go and took it from 7 tests to 22; #2196 took
it to 63, and master now carries 71 top-level tests. Every item this task originally listed is
covered: vote and its failure path (`vote_test.go`), edit inside and outside the deadline, delete
it to 63, and master now carries 70 runnable top-level tests. Every item this task originally listed
is covered: vote and its failure path (`vote_test.go`), edit inside and outside the deadline, delete
and reply (`comment_test.go`), sort change and collapse persistence (`thread_test.go`), anonymous
and email auth (`auth_test.go`), the profile iframe and last-comments (`widgets_test.go`).
@@ -630,11 +644,19 @@ subscription flows. R1 costs Path B little: the task list below retains the exis
`htmx:configRequest`, and `request` in `fetcher.ts` already turns `X-JWT` into the partitioned pair,
so Path B can keep that writer or attach the same handling to a response hook.
The upstream `Partitioned` work is the better answer for either path. With `PartitionedCookies` the
server sets the pair itself and the `JWT` stays `HttpOnly`, so the session survives third-party
blocking without the token ever becoming readable from script, where the header route buys the same
persistence by giving that readability away. Neither path depends on the change and both are better
with it.
The upstream `Partitioned` work is the better answer for the flows that can use it. With
`PartitionedCookies` the server sets the pair itself and the `JWT` stays `HttpOnly`, so the session
survives third-party blocking without the token ever becoming readable from script, where the header
route buys the same persistence by giving that readability away.
Two limits keep that from being a free win. The option is global, so `Service.Set` applies it to the
OAuth callback too, and a cookie partitioned to the popup's own top-level context is one the frame
cannot see: turning it on regresses OAuth on the permissive browsers where an unpartitioned
`SameSite=None` cookie works today, so it wants flow scoping or a separate OAuth answer. And the
security gain is narrower than "no XSS exposure": `HttpOnly` stops a script extracting and replaying
the bearer token, but the XSRF value stays readable by design, and script on the widget origin can
still make authenticated requests that the browser attaches the `HttpOnly` cookie to. What it
removes is token theft, not authenticated action during an XSS.
On one contributor the realistic figure is long enough that the plan's own warning applies to the
schedule and not only to the design. Deletes the entire npm toolchain. **Against it**: 2,400 lines
of the most stateful code get rewritten; a second HTML-fragment API surface becomes permanent