From d5ede91a9fff7042b9085fe976a839996b5b850c Mon Sep 17 00:00:00 2001 From: Dmitry Verkhoturov Date: Mon, 24 Aug 2026 01:48:12 +0100 Subject: [PATCH] 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. --- .../2026-08-19-frontend-simplification.md | 60 +++++++++++++------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/docs/plans/2026-08-19-frontend-simplification.md b/docs/plans/2026-08-19-frontend-simplification.md index 159c07ee..4ad983de 100644 --- a/docs/plans/2026-08-19-frontend-simplification.md +++ b/docs/plans/2026-08-19-frontend-simplification.md @@ -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